PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : OOP und Funktionen als Parameter?


Vedek Bareil
2003-01-25, 00:07:20
Hallo,

ich habe folgendes Problem:
unter C++ besteht ja die Möglichkeit, einer Funktion func1 als Parameter einen Pointer auf eine zweite Funktion func2 zu übergeben, etwa so:

double func2(double); // func2 deklarieren
double func1(double (*func)(double)); // func1 deklarieren
// ...
double y = func1(&func2); // Pointer auf func2 als Parameter an func1

z.B. kann func2 irgendeine mathematische Funktion sein, und func1 z.B. das Integral über die als Parameter übergebene Funktion func2 berechnen.
Nun klappt das so aber offenbar nicht mehr, wenn func2 Member einer Klasse ist:

double func1(double (*func)(double)); // func1 deklarieren
// ...
class Klasse{ // Klasse deklarieren
public:
double func2(double);
void machwas();
double y;
};
// ...
void Klasse::machwas(){
y = func1(&func2); // ergibt eine Fehlermeldung!
}

Und zwar lautet die Fehlermeldung: "taking the address of a non-static member function". Wenn ich nun func2 als static deklarieren, dann geht es, nur leider hat das zur Folge, daß func2 nicht mehr auf nicht-statische Member der Klasse zugreifen kann.

Ich habe mir überlegt, daß es vielleicht klappen würde, wenn func1
ebenfalls Member der Klasse wäre, dann jedoch geht es genauso wenig.

Kürzlich habe ich gelesen, daß Funktionspointer als Parameter ein Mittel seien, im Rahmen konventioneller prozeduraler Programmierung Möglichkeiten der objektorientierten Programmierung nachzuahmen. Tatsächlich aber scheint es mir eher so zu sein, daß es sich um eine Funktionalität handelt, die in der OOP gar nicht zur Verfügung steht!

Demirug
2003-01-25, 00:35:34
Was du suchst sind "Zeiger auf Methoden" bzw "Zeiger auf Elemente".

Im Bezug auf dieses Sprachelement gibt es aber 3 Regeln:

1. Denken sie darüber nach ob sie es wirklich tun wollen
2. Denken sie nochmal darüber nach
3. Wenn sie immer noch der Meinung sind das sie es tun wollen besuchen sie bitte einen Nervenklinik ihrer Wahl

Ich habe diese Technik bisher ein einziges mal benutzt und das auch nur weil man alles irgendwann mal ausprobieren sollte. Als Ergebniss kann ich nur sagen das die Regeln stimmen. Das ganze ist unhandlich und sieht auch noch unschön aus. Für den Fall das du aber die Finger nicht davon lassen kannst: http://www.dracat.net/~wyvern/papers/cppmagic.pdf

Die OOP Lösung für diese Problem nennt sich Interface.


class ICalc
{
public:
virtual double Calc (double val) = 0;
};

double func1(ICalc& calc)
{
return calc.Calc (5.0);
};

class Klasse : ICalc
{
public:
virtual double Calc (double val)
{
return val;
}

void machwas()
{
y = func1 (*this);
}
double y;
};

zeckensack
2003-01-25, 00:41:59
Ack @ Demirug :D
______________
Aber trotzdem:

// ...
class Klasse{ // Klasse deklarieren
public:
double func2(double);
void machwas();
double y;
};

typedef double (Klasse::*member_pointer)(double);
double func1(member_pointer); // func1 deklarieren

// ...
void Klasse::machwas(){
y = func1(&func2); // ergibt eine Fehlermeldung!
}

Xmas
2003-01-25, 02:17:45
Ich hab mal etwas rumprobiert. Wenn es eine Funktion sein soll, die ein Integral berechnet, aber sowohl "einfache" Funktionen als auch Memberfunktionen akzeptieren soll, so könnte das folgende helfen. Im Gegensatz zu Demirugs Vorschlag mit den Interfaces musst du dann nicht jeder Klasse die du benutzen möchtest das Interface hinzufügen.


template < class _T >
class TCalc
{
public:
TCalc( _T& i, double (_T::*f)( double ) ) : instance(i), fpn(f) {}
double calc( double d ) { return (instance.*fpn)( d ); }
private:
_T& instance;
double (_T::*fpn)( double );
};

template < class _T >
double Integral( TCalc<_T>& tc )
{
return tc.calc( 1.5 );
}

double Integral( double (*fpn)( double ) )
{
return (*fpn)( 1.5 );
}

Das ist jetzt ein Beispiel für eine Funktion Integral, die sowohl normale Funktionen als auch Memberfunktionen akzeptieren kann. Der Aufruf geht dann folgendermaßen:


double half( double d )
{
return d * 0.5;
}

class Klasse{ // Klasse deklarieren
public:
double irgendwas( double d ) { return d * 2; }
};


int main()
{
Klasse K;
cout << Integral( TCalc<Klasse>( K, Klasse::irgendwas ) ) << endl;
cout << Integral( half );
return 0;
}

Vedek Bareil
2003-01-25, 02:40:10
Originally posted by Demirug
Im Bezug auf dieses Sprachelement gibt es aber 3 Regeln:

1. Denken sie darüber nach ob sie es wirklich tun wollen
2. Denken sie nochmal darüber nach
3. Wenn sie immer noch der Meinung sind das sie es tun wollen besuchen sie bitte einen Nervenklinik ihrer Wahl tja, daß ich das wirklich unbedingt tun will, ist ja nicht gesagt...
Was ich tun will, ist folgendes: ich habe einenSatz an mathematischen Funktionen func1, func2, ..., funcN, und möchte mit jeder davon eine bestimmte Operation durchführen, z.B. integrieren oder Nullstellen bestimmen.
Und weil ich natürlich brav C++ konform proggen will ;), sollen diese Funktionen Methoden einer Klasse (z.B. CMathFunc) oder auch mehrerer Klassen (etwa CTrigonometric, CHyperbolic, CRational, CExponential) sein.
Wenn du mir zum Umsetzen dieses Vorhabens eine Alternative zu dieser Pointer-auf-Methoden-Sache anbieten kannst, dann schieß los :)

Ich hab's jetzt mal folgendermaßen gemacht:

class CMathFunc{
public:
double func1(double);
// ...
double integrate(CMathFunc::*func)(double); // integriere func
// void machwas();
double y;
};
//...
void CMathFunc::machwas(){
y = integrate(&CMathFunc::func1); // y = Integral über func1
}

d.h. ich habe die Funktion, an die der Methoden-Pointer übergeben werden soll, ebenfalls als Methode der gleichen Klasse deklariert. So wird es zumindest schon mal vom Syntax-Check akzeptiert :)

Das geht natürlich nur so lange wie ich an integrate nur Methoden von CMathFunc übergebe. Will ich Funktionen aus mehrere Klassen übergeben, muß ich die alle von einer Basisklasse ableiten, wobei integrate dann Methode dieser Basisklasse ist.
Jedenfalls dachte ich das bis gerade eben. Da habe ich mir nämlich nochmal deine Codebeispiel genauer durchgelesen und festgestellt, daß es offenbar etwas ganz anderes besagt :D
Die Funktion, die du als Parameter übergibst, ist Calc, und das geht nur deswegen, weil die Funktion func1, an die Calc übergeben wird, ICalc& als Parametertyp hat, und Calc eine Methode der Basisklasse ICalc ist. Wäre Calc nur eine Methode einer von ICalc abgeleiteten Klasse, nicht aber von ICalc selbst, würde es nicht funktionieren.

Demirug
2003-01-25, 09:41:43
Ich übergebe keine Methode sondern ein ein Interface. Ein Interface im C++ Kontext ist eine Klasse welche nur abstrakte Methoden enthält.

Der Unterschied dabei ist das ein Interface zwei Informationen trägt: Das Object und die erlaubten Methodeaufrufe

Ein Methodenzeiger enthält nur die Stelle im Speicher an welcher der Code zu finden ist. Deswegen braucht man als Zusatzinformation noch das Object welches beim Aufruf der Methode als this übergeben werden soll. Xmas hat dies ja schön in eine Templateklasse verpackt. Diese Technik ist manchmal ganz parktisch weil man dann nicht jedesmal eine neue Klasse wegen nur einer Funktion anlegen muss. Allerdings bin ich wie gesagt kein Freund von Methodezeiger. In C# hatte man übrigens ein einsehen und hat sogenate Delegates eingeführt welche das gleiche ermöglichen aber viel angenehmer in der Anwendung sind.

Aber zurück zu deinem Problem. Wenn ich dich richtig verstanden haben möchtest du eine Sammlung von Mathematischen Funktionen haben welche als Parameter jeweils eine beliebige Methode übergeben bekommen sollen welche für jedes x das dazugehörigen y ausrechent.

Das Problem läst sich sowohl mit den Ansatz von Xmas wie auch der Interface Technik lösen. Bei der Interface Technik benutzt man ein Interface welches als Platzhalter für die Funktion dient.


class IFunction
{
public:
virtual double Calc (double x) = 0;
};


Die Math Klasse bekommt dann einen Satz von Funktionen welche die Berechnungen durchführen.


class Math
{
double integrate (IFunction func);
double sum (IFunction func);
...
};


Die Datenquellen sind dann Klassen welche das Interface IFunction implementieren.


class Source1 : IFunction
{
public:
virtual double Calc (double x);
};

class Source2 : IFunction
{
public:
virtual double Calc (double x);
};


In der Anwendung sieht das dann so aus


void Test ()
{
Source1 s1;
Source2 s2;

Math math;

double y1 = math.intergrate (s1);
double y2 = math.intergrate (s2);
double y3 = math.sum (s1);
double y4 = math.sum (s2);
}


Natürlich wäre es wohl noch sinnvoll den Funktionen den Zahlenbereich und die Schrittweite zu übergebn.

Vedek Bareil
2003-01-25, 20:17:57
Originally posted by Demirug
Ich übergebe keine Methode sondern ein ein Interface. das Problem bleibt das gleiche ;)
Ich kann aus jeder Klasse nur eine einzige Funktion übergeben (oder in jeder Klasse nur eine einzige Funktion als Interface implementieren, wenn du so willst). Nehmen wir z.B. an, ich habe eine Klasse CTrigonometric, die eine Reihe trigonometrischer Funktionen, wie sin, cos usw. enthält. Dann kann ich nur eine einzige davon als dieses Interface virtual double Calc realisieren, etwa so:

class CTrigonometric: IFunction
{
public:
virtual double Calc(double x){return sin(x);}; /* berechnet sin(x) und kann an
math.integrate übergeben werden */
double cos(double x); /* berechnet cos(x) und kann nicht übergeben werden */
double tan(double x); /* dito */
}

wenn ich neben sin auch cos und tan übergeben wollte, müßte ich für jede dieser Funktionen eine eigene Klasse konstruieren, wie du richtig erkannt hast:

Source1 s1; /* Klasse für eine zu übergebende Funktion */
Source2 s2; /* Klasse für eine andere zu übergebende Funktion */

Math math;

double y1 = math.intergrate (s1); /* Übergabe der ersten Funktion */
double y2 = math.intergrate (s2); /* Übergabe der zweiten Funktion */

was für meine Zwecke aber ungeeignet ist. Es sollten schon noch mehrere zu übergebende Funktionen in einer einzigen Klasse sein dürfen.

Demirug
2003-01-25, 20:51:55
ok, ich hatte nicht so ganz verstanden was du vorhast. Jetzt ist es klarer. Wenn mir der Nutzen dieser ganzen Sache auch nicht klar ist hast du nur zwei alternativen das ganze zu lösen:

Methodezeiger oder
eine Funktion pro Klasse.

Da ich wie gesagt Methodezeiger nicht mag (sie erinnern mich zu sehr an die üble C Programmierei) mache ich noch einen Versuch dich von der Interfacemethode zu überzeugen :)


class ICalc
{
public:
virtual double Calc (double x) = 0;
};

double func1(ICalc& calc)
{
return calc.Calc (5.0);
};

class CTrigonometric
{
protected:
class SinFunc : public ICalc
{
virtual double Calc (double x)
{
return sin (x);
}
};

class CosFunc : public ICalc
{
virtual double Calc (double x)
{
return cos (x);
}
};

public:
static SinFunc Sin;
static CosFunc Cos;

};

int _tmain(int argc, _TCHAR* argv[])
{
double x1 = func1 (CTrigonometric::Sin);
double x1 = func1 (CTrigonometric::Cos);
}

Vedek Bareil
2003-01-26, 20:31:11
Also ich bin jetzt mal den Vorschlag von XMas nachgegangen, weil mir der am vielversprechendsten erscheint.
Allerdings ist da eine kleine Modifikation vonnöten, so:

Klasse K;
cout << Integral( TCalc<Klasse>( K, Klasse::irgendwas ) ) << endl;

klappt's nämlich nicht ;)
Vielmehr muß zuerst TCalc<Klasse> instanziiert werden, und dann erst die Instanz übergeben werden:

Klasse K;
TCalc<Klass> Calc(K, Klasse::irgendwas);
cout << Integral(Calc);

Außerdem habe ich herausgefunden, daß Templates offenbar eine sehr unästhetische Eigenschaft haben:
normalerweise mache ist das ja so, daß ich Klassen und Funktionen in einer Header-Datei deklariere und die zugehörigen Definitionen in eine seperate cpp-Datei schreibe. Aber bei Templates scheint das nicht zu funktionieren, da hagelt es (unter DevC++ 5 beta 4.9.7.0)immer Linker-Errors, von wegen undefined reference, wenn ich das so mache. Wie es aussieht, muß ich das dann wohl so machen, daß ich entweder die Definition zusammen mit der Deklaration in die Header-Datei schreibe, oder beide zusammen in eine cpp-Datei packe und dann per extern auf sie zugreife. Was aber beides in ziemlichem Widerspruch zur C++ Konformität stehen dürfe...

Edit: es sieht so aus, daß von diesen beiden Möglichkeiten nur eine bleibt. Es ist offenbar nicht möglich, in einer cpp-Datei eine Klasse zu instanziieren, die in einer anderen cpp-Datei deklariert wurde, wenn es keine Header-Datei als Schnittstelle gibt: Instanziierungen der Form

extern Klassenname Objekt //Deklaration von Klassenname in anderer Datei

scheinen nicht akzeptiert zu werden. Es bleibt also nur noch die Möglichkeit, die Funktions/Klassen-Definition mit in die Header-Datei zu schreiben.

Xmas
2003-01-27, 07:17:17
Originally posted by Vedek Bareil
Also ich bin jetzt mal den Vorschlag von XMas nachgegangen, weil mir der am vielversprechendsten erscheint.
Allerdings ist da eine kleine Modifikation vonnöten, so:

Klasse K;
cout << Integral( TCalc<Klasse>( K, Klasse::irgendwas ) ) << endl;

klappt's nämlich nicht ;)
Vielmehr muß zuerst TCalc<Klasse> instanziiert werden, und dann erst die Instanz übergeben werden:

Klasse K;
TCalc<Klass> Calc(K, Klasse::irgendwas);
cout << Integral(Calc);


Eigenartig. Der Code den ich gepostet habe wird von VC.net anstandslos kompiliert. Scheinbar hat DevC++ ein paar Probleme mit Templates. Aber das scheinen die meisten Compiler zu haben ;)

Außerdem habe ich herausgefunden, daß Templates offenbar eine sehr unästhetische Eigenschaft haben:
normalerweise mache ist das ja so, daß ich Klassen und Funktionen in einer Header-Datei deklariere und die zugehörigen Definitionen in eine seperate cpp-Datei schreibe. Aber bei Templates scheint das nicht zu funktionieren, da hagelt es (unter DevC++ 5 beta 4.9.7.0)immer Linker-Errors, von wegen undefined reference, wenn ich das so mache. Wie es aussieht, muß ich das dann wohl so machen, daß ich entweder die Definition zusammen mit der Deklaration in die Header-Datei schreibe, oder beide zusammen in eine cpp-Datei packe und dann per extern auf sie zugreife. Was aber beides in ziemlichem Widerspruch zur C++ Konformität stehen dürfe...
Es ist tatsächlich nicht möglich, eine Template-Definition in ein .cpp-Modul zu packen und das getrennt zu kompilieren. Außer du wüsstest vorher für welche Klassen das Template instantiiert werden soll, und gibst das in der Datei an. Was aber wiederum dem Zweck der Templates entgegenläuft.

Der Compiler kann ja erst Code für das Template generieren, wenn feststeht für welche Typen das Template benötigt wird.

Ich finde es allerdings nicht schlimm, Templates in einer Header-Datei zu definieren. Für Klassen die größtenteils aus "Einzeilern" bestehen schreibe ich auch keine cpp-Datei.

Vedek Bareil
2003-01-31, 00:08:04
kurze Frage nochmal zur Lösung mit Templates:
sollte nicht auch folgendes gehen:

template < class _T >
double Integral(_T& instance, double(_T::*f)(double))
{
return (instance.*f)(1.5);
}


int main()
{
Klasse K;
cout << Integral(K, &Klasse::irgendwas) << endl;
return 0;
}

das würde ohne die Template-Klasse TCalc auskommen und wäre wesentlich kürzer ;)

Xmas
2003-01-31, 01:38:02
Ja das funktioniert. Ich hatte das oben anders gemacht weil ich eigentlich ein anderes Ziel hatte, nämlich eine Funktion Integral zu schreiben die sowohl Methoden als auch normale Funktionen akzeptieren kann, und diese identisch behandelt (also gekapselt in einer Klasse). Mir ist dann aber erst später klargeworden dass das in dieser Weise nicht möglich ist. Nur habe ich dann vergessen den Rest wieder zu vereinfachen.