Привет 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
Хорошо, но почему нет результата, если я начну просто с включения 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 { ... }
У вас есть циклические ссылки между файлами заголовков.
Если 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
.
Всякий раз, когда вы имеете дело с объявлениями, которые используют только указатели/ссылки на тип, а не какие-либо члены этого типа, вы должны предпочесть предварительное объявление для этого типа, а не файл заголовка. Это упростит работу компилятора и позволит избежать возможных циклических проблем.
У вас есть круговые включения. Это не сработает. Удалите
#include "Variable.h"
и#include "Value.h"
изExpression.h
, замените их предварительными объявлениями.