Dynamiczna zmiana działania klasy przy pomocy funkcji lambda

0

Próbuję zaprojektować klasę, w której sposób obróbki danych (pól) może być wielokrotnie zmieniony w locie, przez cały cykl życia programu. Póki co skleciłem coś takiego:

template<typename T>
class Box
{
private:
	void (*Script)(Box& This, const float& fValue) = nullptr;
public:
	T A = static_cast<T>(0);
	T B = static_cast<T>(0);
	T C = static_cast<T>(0);
	void SetScript(auto&& pScript)
	{
		Script = pScript;
	}
	void Update(const float& fValue)
	{
		Script(*this, fValue);
	}
	void Print()
	{
		std::cout << A << std::endl;
		std::cout << B << std::endl;
		std::cout << C << std::endl;
	}
};

int main()
{
    Box<int> box;
    box.SetScript
	(
		[](Box<int>& This, const float& fValue)
		{
			This.A = 100 * fValue;
			This.B = 200 + fValue;
			This.C = 300 - fValue;
		}
	);

	box.Update(87.732);
	box.Print();
}

Niby działa, ale nie jest to rozwiązanie idealne. Przede wszystkim taka lambda nie ma dostępu do niepublicznych pól klasy, więc wszystko musi być widoczne publicznie. Brakuje jej też dostępu do zewnętrznych danych, a więc np. do innych zmiennych czy obiektów (za wyjątkiem odgórne przewidzianych argumentów pokroju fValue).

Kombinowałem też z metodą zamiast funkcji void (Box::*Script)(Box& This, const float& fValue), ale do tego lambda chyba nie może się podczepić(?).

Ktoś ma jakiś lepszy pomysł?

2

Wzorzec strategia...
Spakuj A,B,C do wewnętrznej struktury
struct ABC { T A,B,C; ABC():A(T{}),B(T{}),C(T{}) {}
a zamiast A,B,C daj ABC abc;
do tego:

using Script=fuction<void(ABC&,float)>;
Script script;
void SetScript(Script script) { this->script=script; }
void Update(float value) { script(abc,value); }

Z tym że czemu nie zrobisz zwyczajnie:
void Update(Script script,float value) { script(abc,value); }
Czyli zamiast script przechowywać to przekazać go do update.

0
_13th_Dragon napisał(a):

Wzorzec strategia...
Spakuj A,B,C do wewnętrznej struktury
struct ABC { T A,B,C; ABC():A(T{}),B(T{}),C(T{}) {}
a zamiast A,B,C daj ABC abc;
do tego:

using Script=fuction<void(ABC&,float)>;
Script script;
void SetScript(Script script) { this->script=script; }
void Update(float value) { script(abc,value); }

No i co zyskuję na takim rozwiązaniu?

Z tym że czemu nie zrobisz zwyczajnie:
void Update(Script script,float value) { script(abc,value); }
Czyli zamiast script przechowywać to przekazać go do update.

Nie mogę, bo Update() jest tu publiczny tylko dla testu. Normalnie jest to metoda prywatna, wywoływana przez osobny wątek. Użytkownik aktualizuje jedynie skrypt (dlatego musi być publiczne SetScript()), a reszta sama się kręci, aż do momentu nadpisania skryptu na nowo.

1

No strategia tu pasuje.
Stwórz sobie ogólna klasę "Script" (może być abstrakcyjna) w której będzie wszystko co potrzebne do uruchomienia skryptu (czyli pewnie jedna funkcja). Następnie stwórz klasy odpowiedzialne za konkretne implementacje skryptów, które dziedziczą po "Script", np "ScriptA".
No i tworzysz funkcje w której w switchu będziesz sobie wybierał obiekt którego algorytmu stworzyć a następnie wywołujesz. To jest najprostsze, lopatologiczne rozwiązanie.

0
Crow napisał(a):

No i co zyskuję na takim rozwiązaniu?

Uproszczenie kodu oraz to o co pytałeś: A,B,C mogą być prywatne.

#include <iostream>
#include <functional>
using namespace std;

template<typename T> class Box;

template<typename T> struct ABC
{
    Box<T> &box;
	T A,B,C;
	ABC(Box<T> *box):box(*box),A(T{}),B(T{}),C(T{}) {}
};

template<typename T> class Box
{
	public:
	using Script=function<void(ABC<T>&,float)>;
	private:		
	Script script;
	ABC<T> abc;
	public:
    int whatever;
    Box():abc(*this),whatever(0) {}
	void SetScript(Script script) { this->script=script; }
	void Update(float value) { script(abc,value); }
	void Print()
	{
		cout << abc.A << endl;
		cout << abc.B << endl;
		cout << abc.C << endl;
        cout << whatever << endl;
	}
};

int main()
{
    Box<int> box;
    box.SetScript
	(
		[](ABC<int> &abc,float value)
		{
			abc.A = 100 * value;
			abc.B = 200 + value;
			abc.C = 300 - value;
            ++abc.box.whatever;
		}
	);
	box.Update(87.732);
	box.Print();
	return 0;
}
3

Przede wszystkim taka lambda nie ma dostępu do niepublicznych pól klasy

Lambda w C++ to typ z przeładowanym operatorem() więc mógłbyś zaprzyjaźnić swoją klasę z typem lambdy jeśli ustawiałbyś ją przez konstruktor. https://wandbox.org/permlink/2uheyrrK6Ywz1ZWk

template<typename Callable>
struct Type
{

Callable func;
friend decltype(func);

Type(Callable &&callable, int privInteger) : func{callable}, integer{privInteger} {}

void call() { func(*this); };

private:
 int integer{0};
};

int main()
{
    Type type (
        [](const auto &t) { std::cout << t.integer << "\n"; },
        5
    );  

    type.call();

  return 0;
}

(edit)
W sumie nie musisz przez konstruktor, możesz też przez osobną metodę, ale typ lambdy musisz znać w trakcie tworzenia obiektu.

0
Czitels napisał(a):

No strategia tu pasuje.
Stwórz sobie ogólna klasę "Script" (może być abstrakcyjna) w której będzie wszystko co potrzebne do uruchomienia skryptu (czyli pewnie jedna funkcja). Następnie stwórz klasy odpowiedzialne za konkretne implementacje skryptów, które dziedziczą po "Script", np "ScriptA".
No i tworzysz funkcje w której w switchu będziesz sobie wybierał obiekt którego algorytmu stworzyć a następnie wywołujesz. To jest najprostsze, lopatologiczne rozwiązanie.

Pewnie tak, ale jak rozumiem, wtedy musi istnieć zamknięta, odgórnie zdefiniowana lista takich skryptów, a mi chodzi o coś bardziej ala pseudo język skryptowy, gdzie trzymając się ogólnej struktury danej funkcji, mogę ją sobie dowolnie modyfikować.

Może na przykładzie, żeby przedstawić do czego mi to konkretnie potrzebne. Chodzi o silnik gry, w którym logika gry jest odświeżana niezależnie od renderowania (oba mają własne wątki i własne pętle), ale współdzielą pewne informacje.

Logika gry jest karmiona kodem poprzez abstrakcyjną metodę Update(), która jest nadpisywana zależnie od gry. Załóżmy wiec, że świecie gry istnieje jakiś tam Object, który ma pole Value, a między kolejnymi aktualizacjami logiki wartość ta musi wzrosnąć o 60, a więc:

void Update() override
{
    Object.Value.Set(Object.Value + 60);
}

Słowem wyjaśnienia. Value to instancja klasy szablonowej, która jest wrapperem dla prostego typu (int, float, char itd., reszta jest wykluczona przy pomocy conceptów). Wywołanie Set() nie tylko aktualizuje wartość zmiennej, ale też zakulisowo generuje i kolejkuje instrukcje dla Renderera, który posiada własną kopię wszystkich obiektów świata podlegających rysowaniu. Standardowa instrukcja wygląda tak (po wykonaniu kilku wstępnych wyliczeń):

void Script(auto& This, const float& fDelta)
{
    This.Value = This.OldValue + (This.Factor * fDelta);
    //Factor to wyliczony współczynnik przesunięcia, różnica między starą i nową wartością, w tym przypadku 60.
    //Delta to oczywiście współczynnik upływu czasu rzeczywistego liczony w sekundach i mieszczący się w przedziale 0.0f - 1.0f.
}

Dzięki takiej konstrukcji, Renderer może interpolować miedzy starą a nową wartością i rysować klatki pośrednie (bo na jeden update logiki gry zwykle przypada wiele klatek). Tyle tylko, że taka "ślepa" interpolacja nie zawsze się sprawdza, np. w przypadku portali czy nagłych przesunięć obiektów (których przemieszczenie nie jest rozłożone w czasie i muszą natychmiastowo przejść od jednej wartości do drugiej).

I tu właśnie pojawia się możliwość nadpisania skryptu standardowego przy pomocy lambdy. Np.

void Update() override
{
    Object.Value.Set(Object.Value + 60);
    Object.Value.SetRenderScript
    (
        [](auto& This, const float& fDelta)
        {
            if (n::World[Skeleton67].PosX.Value > 123) This.Value = n::World[Tree11].PosY.Value * fDelta;
            //n::World to statyczny wskaźnik na instancję świata, którą włada Renderer. Dzięki temu jest dostępny z wnętrza lambdy.
        }
    );
}

Chodzi o możliwość korygowania pewnych sytuacji, w których zwykła interpolacja nie wystarcza. Z tego co czytałem, normalnie coś takiego wymaga implementacji języka skryptowego, ale to sporo roboty, więc szukam drogi na skróty :).

1 użytkowników online, w tym zalogowanych: 0, gości: 1