LINUX.ORG.RU

История изменений

Исправление 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 (то есть не обращающиеся к менеджеру памяти) могут быть вызваны очень много раз (так что не хочется двух косвенных переходов и более)