Как я могу добавить функцию-член в унаследованный класс, которому она нужна, не затрагивая другие классы-братья при сохранении полиморфизма?

#c

#c

Вопрос:

Я хочу добавить Token *get_left() const; в class OPERATOR:public Token{} , но ради полиморфизма мне нужно сделать это: class Token {virtual Token *get_left() const = 0;} .Это нормально, но поскольку class Token {} есть другие унаследованные классы, компилятор заставляет меня реализовать эту функцию для всех унаследованных классов. Есть ли способ иметь только эту функцию в классе, которому она нужна, при сохранении полиморфизма?

Token.h

 class Token {
protected:
    Type type;
    Token *Next;
    Token(Type type);
public:
    Type get_type() const;
    void set_type(Type type);
    virtual char *get_value() const = 0;
    virtual int  get_len() const = 0;
    virtual Token *next() = 0;
    virtual void set_next(Token *tok_ptr) = 0;
    virtual Token *get_left() const = 0;
    virtual void set_left(Token *tok_ptr) = 0;
};
  

Operator.h

 class OPERATOR:public Token {
private:
    char  *value;
    Token *left, *right;
    int   len;
public:
    OPERATOR(char *value);
    ~OPERATOR();
    char *get_value() const;
    void set_value(char *value);
    int get_len() const;
    void set_len(char *value);
    Token *get_left() const;
    void set_left(Token *tok_ptr);
    Token *get_right() const;
    void set_right(Token *tok_ptr);
    Token *next();
    void set_next(Token *tok_ptr);
};
  

STRING.h

 class STRING: public Token {
private:
    int  len;
    char *value;
public:
    STRING(char *str);
    ~STRING();
    int get_len() const;
    void set_len(char *str);
    char *get_value() const;
    void set_value(char *str);
    Token *next();
    void set_next(Token *tok_ptr);
};
  

Ответ №1:

Над этим вопросом нужно немного поработать. У нас есть только ваши объявления, и важно, как и когда следует вызывать «get_left» и «get_right». Похоже, вы пишете интерфейс для синтаксического анализа, поэтому в качестве примечания я бы рекомендовал вам изучить yacc (или bison или один из его вариантов) и lex (или flex или один из его вариантов).). Но давайте перейдем к вопросу C . Если имеет смысл говорить только о «get_left» и «get_right» для определенного подкласса, полиморфизм как метод / концепция не требует, чтобы у вас были все методы, которые вы используете на каждом уровне вашей абстракции в базовом классе. Я собираюсь упростить ваш код, чтобы проиллюстрировать:

 enum Type
{
  string_type = 0,
  operator_type,
  ... // the rest
};

class token {
protected:  // stuff all tokens have (I guess)
  Type type; // side-note: if polymorphism is used correctly, a "type" field should not be needed
  std::string value; // they all seem to need value as well, so we put it here
  token *next; // all objects of type "token" need to be able to belong to linked list (let's say)

public:

  token(Type _type, const char* _value)
    : type(_type)
    , value(_value)
    , next(NULL)
  {}

  // if they share the above data, there's no real reason to make these
  // virtual, let alone abstract. If there REALLY is a different way
  // that a string returns its value as opposed to how an operator returns
  // it's value, then I guess you'd want to make a virtual function out
  // of the accessor, but you still may have a default implementation if
  // most tokens simply return the contents of their value string.

  Type get_type() const { return type; }
  void set_type(Type type) { this.type = type; }
  const char* get_value() const { return value.c_str(); }
  std::size_t get_len() const { return value.length(); }
  token* next() const { return next; }

  virtual void process() = 0;
};
  

Давайте остановимся на этом. Именно здесь становится важным понимать больше, чем просто интерфейсы. Поток управления так же важен, как и определения классов, для того, как работает полиморфизм. Существует только одна абстрактная функция — process. Это потому, что для простоты предположим, что существует сканер шаблонов строк, который идентифицирует токены, классифицирует их и создает то, что выглядит как связанный список объектов, все из которых основаны на token, но каждый из которых является экземпляром конкретного класса. Как только этот список завершен, мы выполняем итерацию по нему, вызывая метод process для каждого из них, который работает с каждым соответствующим образом. В этом суть полиморфизма. Я не знаю, каков ваш поток управления на самом деле, но если он включает get_left и get_right для объектов, которым не нужны эти операции, вы уже «нарушили полиморфизм». Итак, ваш код сканера примерно —

 1. get next space delimited string
2. use contextual information to decide its type
   a. if type is an operator, create an object of type "operator" with type-specific data.
   b. same for string
   c. same for all other token types.
3. because each of these concrete objects are also instances of the base class
   token, you add each of them to a linked list with a head of type token*. The
   maintenance code for this list (adding, deleting, iterating) only has to
   know that these are all tokens.
4. repeat 1-3 until out of token candidates.
5. iterate through the list of abstract tokens, calling the "process" function.
  

Итак, теперь вот ваш класс operator —

 class operator_token : public token
{
private:
  // these are unique to "operator_token", but it has all the others from "token"
  token* left_operand;
  token* right_operand;

public:
  // the scanner code will be explicitly creating an instance of "operator_token"
  // with its unique constructor and the contextual information it needs,
  // but the code that adds it to the linked list and the code that iterates
  // through that linked list only knows it's a "token".

  operator_token(const char* value, token* left, token* right)
    : token(operator_type, value)
    , left_operand(left)
    , right_operand(right)
  {}

  virtual void process() override
  {
    // just something silly to illustrate. This implementation of "process"
    // can use left_operand and right_operand, because it will only be called
    // on an "operator_token" sub-class of "token".
    std::cout << "expression found: "
         << left_operand.get_value()
         << " " << value << " "
         << right_operand.get_value()
         << std::end;
  }
};
  

И более простой подкласс «string_token» —

 class string_token : public token
{
// no private members because (in my simplified example) strings are pretty
// generic in their data needs
public:
  string_token(const char* value)
    : token(string_type, value)
  {}   // just the basics

  virtual void process() override
  {
     std::cout << "string: " << value << std::end;
  }
};
  

Это полиморфизм. Код, вызывающий метод «process», не знает тип токена, но каждая переопределенная реализация «process» знает и может использовать информацию, относящуюся к классу, в своей работе. Надеюсь, это поможет.

Комментарии:

1. О, боже… Я использовал «operator» в качестве имени класса. Обычно я запускаю код, который я вношу, прежде чем публиковать его (потому что моя врожденная команда синтаксической корректности… гм, ограниченный). Однако, я думаю, я, должно быть, пропустил этот шаг в данном случае, потому что «operator» — это ключевое слово в c , позволяющее перегружать. Я почти уверен, что приведенный выше код приведет к тому, что компилятор сделает грустное лицо, поэтому я редактирую все подклассы «token», чтобы они были «foo_token», так что подкласс «operator» «token» станет «operator_token». Извинения. Не знаю, как это произошло.