История изменений
Исправление KivApple, (текущая версия) :
Переделал вот так:
#include <cstdio>
class BaseAlgorithm;
struct BaseState {
BaseAlgorithm &algorithm;
BaseState(BaseAlgorithm &algorithm): algorithm(algorithm) {
}
void updateState();
};
class BaseAlgorithm {
public:
virtual ~BaseAlgorithm() = default;
virtual BaseState *invokeNewState() = 0;
virtual void invokeUpdateState(BaseState *state) = 0;
virtual void invokeDeleteState(BaseState *state) = 0;
};
void BaseState::updateState() {
algorithm.invokeUpdateState(this);
}
template<typename T, typename S> class Algorithm: public BaseAlgorithm {
public:
BaseState *invokeNewState() final {
return new S(*this);
}
void invokeUpdateState(BaseState *state) final {
static_cast<T*>(this)->T::updateState(static_cast<S*>(state));
}
void invokeDeleteState(BaseState *state) final {
delete static_cast<S*>(state);
}
};
struct MyAlgorithmState: public BaseState {
int a = 10;
MyAlgorithmState(BaseAlgorithm &algorithm): BaseState(algorithm) {
}
~MyAlgorithmState() {
printf("~MyAlgorithmState\n"); // Non-trivial destructor
}
};
class MyAlgorithm: public Algorithm<MyAlgorithm, MyAlgorithmState> {
public:
void updateState(MyAlgorithmState *state) {
state->a++;
printf("%i\n", state->a);
}
};
void test(BaseAlgorithm *algorithm) {
BaseState *state = algorithm->invokeNewState();
state->updateState();
state->updateState();
state->updateState();
state->algorithm.invokeDeleteState(state);
}
int main(int argc, char *argv[]) {
MyAlgorithm algorithm;
test(&algorithm);
return 0;
}
Теперь всё точно хорошо - виртуальные методы и методы наследника называются по-разному, двойной косвенный переход отсутствует (при включенных оптимизациях в invokeUpdateState тупо инлайнится без всяких условий updateState наследника, что и требуется), само состояние лёгковесное (хранит только указатель на algorithm, но не содержит лишнего дополнительного указателя на свой собственный vtable), если в одном из наследников класса состояния есть нетривиальный деструктор, он будет корректно вызван, для алгоритмов работает runtime полиморфизм, при реализации алгоритмов не нужно выполнять руками каст в нужный тип (соответственно, нельзя выстрелить себе в ногу, создав состояние одного типа, а работая с другим - шаблон гарантирует, что все касты состояния будут типобезопасными).
Интересно разве что, можно ли как-то описать прототипы методов, которые программист обязан описать в наследнике класса, чтобы заработал автокомплит в IDE при их определении, а также были более человекочитаемые ошибки компиляции...
P. S.: Я исхожу из нескольких допущений:
- Реализаций алгоритмов может быть много (и поэтому хочется максимизировать удобство написания и убрать ручные касты, где можно опечататься и выстрелить себе в ногу) и они выбираются полностью в рантайме (так что полиморфизм на виртуальных методах таки нужен, нельзя завернуть всю работу с алгоритмами в шаблоны)
- Методов вроде updateState может быть много (см. предыдущий текст в скобках)
- State весит очень мало (несколько байт, поэтому хранение в нём двух указателей вместо одного это плохо)
- State может существовать в количестве сотен тысяч штук (поэтому размер структуры было бы приятно оптимизировать)
- Методы вроде updateState (то есть не обращающиеся к менеджеру памяти) могут быть вызваны очень много раз (так что не хочется двух косвенных переходов и более)
P. P. S.: В реальном проекте я создаю State с помощью placement new в большом заранее выделенном массиве (просто установил максимальный размер State (как я уже сказал, там всегда не больше нескольких байт) и сделал массив длины максимальный размер State * количество State байт, что это правило не нарушается проверяю с помощью static_assert в шаблоне), так что 100500 обращений к аллокатору не происходит.
Исправление KivApple, :
Переделал вот так:
#include <cstdio>
class BaseAlgorithm;
struct BaseState {
BaseAlgorithm &algorithm;
BaseState(BaseAlgorithm &algorithm): algorithm(algorithm) {
}
void updateState();
};
class BaseAlgorithm {
public:
virtual ~BaseAlgorithm() = default;
virtual BaseState *invokeNewState() = 0;
virtual void invokeUpdateState(BaseState *state) = 0;
virtual void invokeDeleteState(BaseState *state) = 0;
};
void BaseState::updateState() {
algorithm.invokeUpdateState(this);
}
template<typename T, typename S> class Algorithm: public BaseAlgorithm {
public:
BaseState *invokeNewState() final {
return new S(*this);
}
void invokeUpdateState(BaseState *state) final {
static_cast<T*>(this)->T::updateState(static_cast<S*>(state));
}
void invokeDeleteState(BaseState *state) final {
delete static_cast<S*>(state);
}
};
struct MyAlgorithmState: public BaseState {
int a = 10;
MyAlgorithmState(BaseAlgorithm &algorithm): BaseState(algorithm) {
}
~MyAlgorithmState() {
printf("~MyAlgorithmState\n"); // Non-trivial destructor
}
};
class MyAlgorithm: public Algorithm<MyAlgorithm, MyAlgorithmState> {
public:
void updateState(MyAlgorithmState *state) {
state->a++;
printf("%i\n", state->a);
}
};
void test(BaseAlgorithm *algorithm) {
BaseState *state = algorithm->invokeNewState();
state->updateState();
state->updateState();
state->updateState();
state->algorithm.invokeDeleteState(state);
}
int main(int argc, char *argv[]) {
MyAlgorithm algorithm;
test(&algorithm);
return 0;
}
Теперь всё точно хорошо - виртуальные методы и методы наследника называются по-разному, двойной косвенный переход отсутствует (при включенных оптимизациях в invokeUpdateState тупо инлайнится без всяких условий updateState наследника, что и требуется), само состояние лёгковесное (хранит только указатель на algorithm, но не содержит лишнего дополнительного указателя на свой собственный vtable), если в одном из наследников класса состояния есть нетривиальный деструктор, он будет корректно вызван, для алгоритмов работает runtime полиморфизм, при реализации алгоритмов не нужно выполнять руками каст в нужный тип (соответственно, нельзя выстрелить себе в ногу, создав состояние одного типа, а работая с другим - шаблон гарантирует, что все касты состояния будут типобезопасными).
P. S.: Я исхожу из нескольких допущений:
- Реализаций алгоритмов может быть много (и поэтому хочется максимизировать удобство написания и убрать ручные касты, где можно опечататься и выстрелить себе в ногу) и они выбираются полностью в рантайме (так что полиморфизм на виртуальных методах таки нужен, нельзя завернуть всю работу с алгоритмами в шаблоны)
- Методов вроде updateState может быть много (см. предыдущий текст в скобках)
- State весит очень мало (несколько байт, поэтому хранение в нём двух указателей вместо одного это плохо)
- State может существовать в количестве сотен тысяч штук (поэтому размер структуры было бы приятно оптимизировать)
- Методы вроде updateState (то есть не обращающиеся к менеджеру памяти) могут быть вызваны очень много раз (так что не хочется двух косвенных переходов и более)
P. P. S.: В реальном проекте я создаю State с помощью placement new в большом заранее выделенном массиве (просто установил максимальный размер State (как я уже сказал, там всегда не больше нескольких байт) и сделал массив длины максимальный размер State * количество State байт, что это правило не нарушается проверяю с помощью static_assert в шаблоне), так что 100500 обращений к аллокатору не происходит.
Исходная версия KivApple, :
Переделал вот так:
#include <cstdio>
class BaseAlgorithm;
struct BaseState {
BaseAlgorithm &algorithm;
BaseState(BaseAlgorithm &algorithm): algorithm(algorithm) {
}
void updateState();
};
class BaseAlgorithm {
public:
virtual ~BaseAlgorithm() = default;
virtual BaseState *invokeNewState() = 0;
virtual void invokeUpdateState(BaseState *state) = 0;
virtual void invokeDeleteState(BaseState *state) = 0;
};
void BaseState::updateState() {
algorithm.invokeUpdateState(this);
}
template<typename T, typename S> class Algorithm: public BaseAlgorithm {
public:
BaseState *invokeNewState() final {
return new S(*this);
}
void invokeUpdateState(BaseState *state) final {
static_cast<T*>(this)->T::updateState(static_cast<S*>(state));
}
void invokeDeleteState(BaseState *state) final {
delete static_cast<S*>(state);
}
};
struct MyAlgorithmState: public BaseState {
int a = 10;
MyAlgorithmState(BaseAlgorithm &algorithm): BaseState(algorithm) {
}
~MyAlgorithmState() {
printf("~MyAlgorithmState\n"); // Non-trivial destructor
}
};
class MyAlgorithm: public Algorithm<MyAlgorithm, MyAlgorithmState> {
public:
void updateState(MyAlgorithmState *state) {
state->a++;
printf("%i\n", state->a);
}
};
void test(BaseAlgorithm *algorithm) {
BaseState *state = algorithm->invokeNewState();
state->updateState();
state->updateState();
state->updateState();
state->algorithm.invokeDeleteState(state);
}
int main(int argc, char *argv[]) {
MyAlgorithm algorithm;
test(&algorithm);
return 0;
}
Теперь всё точно хорошо - виртуальные методы и методы наследника называются по-разному, двойной косвенный переход отсутствует (при включенных оптимизациях в invokeUpdateState тупо инлайнится без всяких условий updateState наследника, что и требуется), само состояние лёгковесное (хранит только указатель на algorithm, но не содержит лишнего дополнительного указателя на свой собственный vtable), если в одном из наследников класса состояния есть нетривиальный деструктор, он будет корректно вызван, для алгоритмов работает runtime полиморфизм, при реализации алгоритмов не нужно выполнять руками каст в нужный тип (соответственно, нельзя выстрелить себе в ногу, создав состояние одного типа, а работая с другим - шаблон гарантирует, что все касты состояния будут типобезопасными).
P. S.: Я исхожу из нескольких допущений:
- Реализаций алгоритмов может быть много (и поэтому хочется максимизировать удобство написания и убрать ручные касты, где можно опечататься и выстрелить себе в ногу) и они выбираются полностью в рантайме (так что полиморфизм на виртуальных методах таки нужен, нельзя завернуть всю работу с алгоритмами в шаблоны)
- Методов вроде updateState может быть много (см. предыдущий текст в скобках)
- State весит очень мало (несколько байт, поэтому хранение в нём двух указателей вместо одного это плохо)
- State может существовать в количестве сотен тысяч штук (поэтому размер структуры было бы приятно оптимизировать)
- Методы вроде updateState (то есть не обращающиеся к менеджеру памяти) могут быть вызваны очень много раз (так что не хочется двух косвенных переходов и более)