Почему я получаю неопределенный базовый класс?

Привет StackOverflow!

Я пытаюсь сделать какой-то алгебраический решающий код. Я изложил некоторую базовую структуру. Но я натыкаюсь на ошибку, что подкласс считает, что базовый класс не определен. Я пытался удалить #pragma once и удалить и изменить операторы include здесь и там. Но я не могу понять это.

Я думаю, что это как-то связано с файлами, включающими друг друга, но из того, что я думал, что знал раньше. Это должно быть возможно благодаря #pragma once и #ifndef. Пожалуйста, посоветуйте мне, что я делаю неправильно здесь!

Выражение.h:

#pragma once
#ifndef MATH_EXPRESSION_H
#define MATH_EXPRESSION_H

#include <string>

#include "Variable.h"
#include "Value.h"

class Expression {

public:
    virtual             Expression*     eval(const Variable* var, Value* val)           = 0;
    virtual operator    std::string()                                           const   = 0;

    template<class T>
    bool                isType() {
        return dynamic_cast<T*>(this);
    }

};

#endif

Переменная.h:

#pragma once
#ifndef MATH_VARIABLE_H
#define MATH_VARIABLE_H

#include "Expression.h"

class Variable : public Expression {
};

#endif

Значение.ч:

#pragma once
#ifndef MATH_VALUE_H
#define MATH_VALUE_H

#include "Expression.h"

class Value : public Expression {

public:
    float value;

    Value(float value) : value(value) {}

public:
    Expression* eval() { return this; }
    Expression* eval(const Variable* v, Value* val) {
        return this->eval();
    }

    operator std::string() const {
        return std::to_string(value);
    }
};

#endif

И это ошибка сборки, которую я пытаюсь решить:

Variable.h(7,36): error C2504: 'Expression': base class undefined

У вас есть круговые включения. Это не сработает. Удалите #include "Variable.h" и #include "Value.h" из Expression.h, замените их предварительными объявлениями.

Igor Tandetnik 13.12.2020 01:07

Хорошо, но почему нет результата, если я начну просто с включения Variable.h: ´#include "Expression.h"` #include <string>/* #include "Variable.h" !!! Shouldn't proceed becuse we already included it*/#include "Value.h"class Expression { ... }class Variable { ... }

Kevin Bäckstäde 13.12.2020 01:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
431
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

У вас есть циклические ссылки между файлами заголовков.

  • Если Expression.h включен в единицу перевода до того, как включены Variable.h и Value.h, Expression.h определит защиту MATH_EXPRESSION_H, а затем включит Variable.h перед объявлением класса Expression. Variable.h снова будет включать Expression.h, что фактически не работает из-за MATH_EXPRESSION_H охраны. Таким образом, класс Variable будет ссылаться на тип Expression, который еще не был объявлен, отсюда и ошибка компилятора.

  • Если Variable.h включен в единицу перевода до включения Expression.h, Variable.h определит MATH_VARIABLE_H защиту, а затем включит Expression.h перед объявлением класса Variable. Expression.h снова будет включать Variable.h, что фактически не работает из-за MATH_VARIABLE_H охраны. Затем Expression.h будет включать Value.h, который снова будет включать Expression.h, что недопустимо (см. выше). Таким образом, класс Value будет ссылаться на тип Expression, который еще не был объявлен, отсюда и ошибка компилятора.

  • Если Value.h включен в единицу перевода до включения Expression.h, Value.h определит MATH_VALUE_H защиту, а затем включит Expression.h перед объявлением класса Value. Expression.h будет включать Variable.h, который снова будет включать Expression.h, что недопустимо (см. выше). Тогда Expression.h будет включать Value.h, что невозможно из-за охраны MATH_VALUE_H. Таким образом, класс Variable будет ссылаться на тип Expression, который еще не был объявлен, отсюда и ошибка компилятора.

Давайте посмотрим на это с реальным кодом.

Если Expression.h включен первым, это то, что увидит компилятор после обработки директив препроцессора:

// content from <string> ...

class Variable : public Expression { // <-- error, Expression not defined!
};

class Value : public Expression { // <-- error, Expression not defined!
   ...
public:
    Expression* eval() { ... } // <-- error, Expression not defined!
    Expression* eval(const Variable* v, Value* val) { ... } // <-- error, Expression not defined!
    ...
};

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- OK!
};

Если Variable.h включен первым:

// content from <string> ...

class Value : public Expression { // <-- error, Expression not defined!
   ...
public:
    Expression* eval() { ... } // <-- error, Expression not defined!
    Expression* eval(const Variable* v, Value* val) { ... } // <-- error, Expression and Variable not defined!
    ...
};

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- error, Variable not defined!
    ...
};

class Variable : public Expression { // <-- OK!
};

Если Value.h включен первым:

// content from <string> ...

class Variable : public Expression { // <-- error, Expression not defined!
};

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- error, Value not defined!
    ...
};

class Value : public Expression { // <-- OK!
public:
    Expression* eval() { ... } // <-- OK!
    Expression* eval(const Variable* v, Value* val) { ... } // <-- OK!
    ...
};

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

Поскольку вы используете только указатели на различные классы в типах параметров функций и типах возвращаемых значений и не имеете доступа к каким-либо членам классов, вы можете разорвать эту циклическую проблему, заменив операторы #include в Expression.h на предварительные объявления, например:

#pragma once
#ifndef MATH_EXPRESSION_H
#define MATH_EXPRESSION_H

#include <string>

// forward declarations
class Variable;
class Value;

class Expression {

public:
    virtual Expression* eval(const Variable* var, Value* val) = 0;
    virtual operator std::string() const = 0;

    template<class T>
    bool isType() {
        return dynamic_cast<T*>(this);
    }

};

#endif

Декларация Expression должна знать только то, что Variable и Value существуют, а не то, как они выглядят внутри. То же самое с объявлением Value, только нужно знать, что Variable существует, а не то, как оно выглядит внутри.

Итак, давайте посмотрим, что это изменение делает с кодом, который видит компилятор после обработки директив препроцессора.

Если Expression.h включен первым:

// content from <string> ...

// forward declarations
class Variable;
class Value;

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- OK!
    ...
};

Если Variable.h включен первым:

// content from <string> ...

// forward declarations
class Variable;
class Value;

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- OK!
};

class Variable : public Expression { // OK!
};

Если Value.h включен первым:

// content from <string> ...

// forward declarations
class Variable;
class Value;

class Expression {
public:
    virtual Expression* eval(const Variable* var, Value* val) = 0; // <-- OK!
    ...
};

class Value : public Expression { // <-- OK!
    ...
public:
    Expression* eval() { ... } // <-- OK!
    Expression* eval(const Variable* v, Value* val) { ... } // <-- OK!
    ...
};

Это означает, что вам придется #include заголовки Variable.h и Value.h в любых других исходных/заголовочных файлах, которым требуется фактический доступ к членам Variable и Value.

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

Другие вопросы по теме