Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Wie verwende ich result_of fuer Member Function Pointer?

60 views
Skip to first unread message

Helmut Zeisel

unread,
Apr 12, 2012, 9:10:24 AM4/12/12
to
Was ist in C++ 2011 im Code unten der korrekte generische Typ fuer
XXXXXX ?

Ich nehme an, das geht irgendwie mit std::result_of, aber wie genau?

Helmut

=============================
#include <memory>

class A
{
public:
A(double a=0):ma(a) {};
double f(double x, double y)
{
return ma*x+y;
}
private:
double ma;
};

template<typename T> class Proxy
{
public:
Proxy(const std::shared_ptr<T>& p): m(p) {}
template<typename U, typename ... Args> XXXXXX forward(U u,
Args ...args)
{
return ((*m).*u)(args...);
}
private:
std::shared_ptr<T> m;
};
int main()
{
Proxy<A> p(std::make_shared<A>());
auto x = p.forward(&A::f,4.,5.);
}

Daniel Krügler

unread,
Apr 12, 2012, 5:18:28 PM4/12/12
to
Viele Wege führen nach Rom. Mit std::result_of geht es so:

template<typename U, typename ... Args>
typename std::result_of<U&(T&, Args&...)>::type
forward(U u, Args ...args)
{
return ((*m).*u)(args...);
}

Wenn du vor decltype nicht zurückschreckst, hier noch ein paar andere Varianten:

Dazu lässt sich es am einfachsten in der neuen Post-Fix-Notation schreiben. Technisch möglich sollte sein:

template<typename U, typename ... Args>
auto forward(U u, Args ...args) -> decltype( ((*(this->m)).*u)(args...) )
{
return ((*m).*u)(args...);
}

aber gcc akzeptiert den Bezug auf "this->m" nicht. Dies lässt sich aber
beheben durch Verwendung von std::declval aus <utility>, womit wir uns einen entsprechenden Wert "erzeugen":

template<typename U, typename ... Args>
auto forward(U u, Args ...args) ->
decltype( ((*(std::declval<std::shared_ptr<T>&>())).*u)(args...) )
{
return ((*m).*u)(args...);
}

Theoretisch ist es möglich, jeden der Objektausdrücke hier durch entsprechende std::declval<X&>() zu ersetzen, das macht es allerdings recht länglich und unleserlich. Als Beispiel hier auch nochmal in der "normalen" Funktionsrückgabewertschreibweise:

template<typename U, typename ... Args>
decltype( ((*(std::declval<std::shared_ptr<T>&>())).*std::declval<U&>())(std::declval<Args&>()...) )
forward(U u, Args ...args)
{
return ((*m).*u)(args...);
}

Viel Erfolg noch und besten Gruss aus Bremen,

Daniel Krügler

Helmut Zeisel

unread,
Apr 13, 2012, 2:48:39 AM4/13/12
to
On Apr 12, 11:18 pm, Daniel Krügler <daniel.krueg...@googlemail.com>
wrote:
> Viele Wege führen nach Rom. Mit std::result_of geht es so:
>
>  template<typename U, typename ... Args>
>  typename std::result_of<U&(T&, Args&...)>::type
>  forward(U u, Args ...args)
>  {
>    return ((*m).*u)(args...);
>  }

Danke, genau das habe ich gemeint. Das klappt allerdings nicht mit gcc
4.5.3 (derzeit aktuelle cygwin-Version), inzwischen hab ich dazu noch
folgendes gefunden:

http://gcc.gnu.org/bugzilla/show_bug.cgi?id=48521

"Jonathan Wakely 2011-04-23 17:54:45 UTC
fixed for 4.6.1
I don't plan to fix this on the 4.5 branch"

> template<typename U, typename ... Args>
>  auto forward(U u, Args ...args) -> decltype( ((*(this->m)).*u)(args...) )
> {
>    return ((*m).*u)(args...);
>
> }
>
> aber gcc akzeptiert den Bezug auf "this->m" nicht.

Hab ich auch versucht: "sorry, unimplemented: use of
`type_pack_expansion' in template"

>Dies lässt sich aber
> beheben durch Verwendung von std::declval aus <utility>, womit wir uns einen entsprechenden Wert "erzeugen":
>
>  template<typename U, typename ... Args>
>  auto forward(U u, Args ...args) ->
>    decltype( ((*(std::declval<std::shared_ptr<T>&>())).*u)(args...) )
>  {
>    return ((*m).*u)(args...);
>  }

Hab ich jetzt auch getestet; gcc 4.5.3 sagt auch hier: "sorry,
unimplemented: use of `type_pack_expansion' in template"

Ich werde wohl den gcc 4.7 compilieren muessen.

Danke nochmals,

Helmut

Helmut Zeisel

unread,
Apr 14, 2012, 12:32:07 PM4/14/12
to
Am Freitag, 13. April 2012 08:48:39 UTC+2 schrieb Helmut Zeisel:
> On Apr 12, 11:18 pm, Daniel Krügler <daniel.krueg...@googlemail.com>
> wrote:
> > Mit std::result_of geht es so:
> >
> >  template<typename U, typename ... Args>
> >  typename std::result_of<U&(T&, Args&...)>::type
> >  forward(U u, Args ...args)
> >  {
> >    return ((*m).*u)(args...);
> >  }
>
> Danke, genau das habe ich gemeint.

Ich hab noch ein wenig herumprobiert. Damit es auch klappt, wenn der Copy Constructor eines der Args geloescht ist, brauche ich

template<typename U, typename ... Args>
typename std::result_of<U&(T&, Args&...)>::type
forward(U u, const Args& ...args)
{
return ((*m).*u)(args...);
}

(evtl auch noch "const U& u", falls U ein Funktionsobjekt sein kann).

Aber brauche ich wirklich

std::result_of<U&(T&, Args&...)>::type; ?

Die Version

std::result_of<U(T, Args...)>::type;

klappt auch - gibt es tatsaechlich Faelle, wo sich die beiden Versionen voneinander unterscheiden?

Helmut

Daniel Krügler

unread,
Apr 15, 2012, 10:46:05 AM4/15/12
to
Am Samstag, 14. April 2012 18:32:07 UTC+2 schrieb Helmut Zeisel:
> Am Freitag, 13. April 2012 08:48:39 UTC+2 schrieb Helmut Zeisel:
> > On Apr 12, 11:18 pm, Daniel Krügler <daniel.krueg...@googlemail.com>
> > wrote:
> > > Mit std::result_of geht es so:
> > >
> > >  template<typename U, typename ... Args>
> > >  typename std::result_of<U&(T&, Args&...)>::type
> > >  forward(U u, Args ...args)
> > >  {
> > >    return ((*m).*u)(args...);
> > >  }
> >
> > Danke, genau das habe ich gemeint.
>
> Ich hab noch ein wenig herumprobiert. Damit es auch klappt, wenn der Copy Constructor eines der Args geloescht ist, brauche ich
>
> template<typename U, typename ... Args>
> typename std::result_of<U&(T&, Args&...)>::type
> forward(U u, const Args& ...args)
> {
> return ((*m).*u)(args...);
> }
>
> (evtl auch noch "const U& u", falls U ein Funktionsobjekt sein kann).

So wie dein Aufruf definiert ist, kann U niemals ein Funktionsobjekt sein, da sich diese nicht dereferenzieren lassen. Aber wenn du "perfekte" Argumentenweiterleitung willst, schreib einfach

template<typename U, typename ... Args>
typename std::result_of<U&(T&, Args&&...)>::type
forward(U u, Args&&...args)
{
return ((*m).*u)(std::forward<Args>(args)...);
}

> Aber brauche ich wirklich
>
> std::result_of<U&(T&, Args&...)>::type; ?
>
> Die Version
>
> std::result_of<U(T, Args...)>::type;
>
> klappt auch - gibt es tatsaechlich Faelle, wo sich die beiden Versionen voneinander unterscheiden?

Es gibt ganz klar Fälle, wo das Ergebnis unterschiedlich wäre. Derzeit sind die meisten dieser Fälle allerdings noch nicht demonstrierbar, da aktuelle Compiler noch nicht alle C++11-Features (insbesondere ref-qualifizierte Element-Funktionen) unterstützen oder weil sie genau in deinem Beispiel nicht auftreten können. Rein technisch wird durch die genaue Form von result_of festgelegt, welcher Wert-Kategorie die Argumente bzw. das "aufrufbare Objekt" (callable object) angehört. Die Werte-Kategorie kann entscheidend sein, weil ja nicht alle Kombinationen gültig sind. In deinem konkreten Beispiel mit der speziellen Funktion f() spielt es keine Rolle, da die Funktion sowohl rvalue als auch lvalues akzeptiert. Dazu kommt, dass zufällig bei dir U nie eine Funktion sein kann, sondern nur ein Element-Zeiger.

In andere Fällen kann man das Problem leichter aufzeigen:

std::result_of<U(T, Args...)>::type

wäre z.B. ungültig, wenn U eine Funktion wäre, da Funktionen nicht als Rückgabetyp einer anderen Funktion verwendet werden können. Eine weitere Subtilität ist, dass durch die Enkodierung von result_of die "Constness" von allen Parametern verloren geht. In diesem Fall ist U& also zwingend. In obigen Beispiel würden T und Args zudem nie konstant sein können, da bei Werte-Funktionsparametern diese aus dem Funktionstyp entfernt werden. Zur Klarstellung:

double f(const double x, const double y)

hat die gleiche Signatur wie

double f(double x, double y)

Dies kann man leicht demonstrieren, wenn wir in deinem Beispiel anstelle von

Proxy<A> p(std::make_shared<A>());

ein

Proxy<const A> p(std::make_shared<A>());

deklarieren. Der result_of-Wert gibt immer noch etwas zurück, obwohl result_of hier nicht definiert sein dürfte, weil du versuchst, eine nicht-konstante Elementfunktion auf ein konstantes Objekt auszuführen. Wenn du danach das T in

std::result_of<U(T, Args...)>::type

zum korrekten T& änderst (Es ist deshalb korrekt, weil T als lvalue aufgerufen wird), meckert auch result_of sofort.

Ursprünglich wurde bei reference_wrapper genau so ein Fehler mit nicht-korrekten Parameter/Rückgabetypen gemacht, siehe:

http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#2017

Ebenso an anderen Stellen im Standard, siehe

http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#2021

Insbesondere das erste Beispiel zeigt, was schieflaufen kann. Deine Definition wird auf jeden Fall scheitern, wenn dein Compiler ref-qualifizierte Funktionen unterstützt, weil dann entscheidend ist, ob die Funktion mit einem rvalue oder einem lvalue aufgerufen wird. Deine obige Definition tut so, als wäre es ein rvalue, was aber nicht mit deinem Code übereinstimmt.

Ich hoffe, die Sache ist etwas klarer geworden.

Besten Gruss aus Bremen,

Daniel Krügler

Helmut Zeisel

unread,
Apr 16, 2012, 8:22:39 AM4/16/12
to
On Thursday, April 12, 2012 11:18:28 PM UTC+2, Daniel Krügler wrote:

> aber gcc akzeptiert den Bezug auf "this->m" nicht. Dies lässt sich aber
> beheben durch Verwendung von std::declval aus <utility>, womit wir uns einen entsprechenden Wert "erzeugen":
>
> template<typename U, typename ... Args>
> auto forward(U u, Args ...args) ->
> decltype( ((*(std::declval<std::shared_ptr<T>&>())).*u)(args...) )
> {
> return ((*m).*u)(args...);
> }

auto forward(U u, Args ...args) ->
decltype( (std::declval<T>().*u)(args...) )

bzw. besser

auto forward(U u, Args&& ...args) ->
decltype( (std::declval<T>().*u)(std::forward<Args>(args)...) )

klappt auch - gibt's da einen subtilen Unterschied zwischen
*(std::declval<std::shared_ptr<T>&>()) und std::declval<T>() ?

Helmut

Helmut Zeisel

unread,
Apr 16, 2012, 1:19:59 AM4/16/12
to
Am Sonntag, 15. April 2012 16:46:05 UTC+2 schrieb Daniel Krügler:

> Ich hoffe, die Sache ist etwas klarer geworden.

Eigentlich bin ich jetzt noch mehr verwirrt ...

Die am einfachsten erklaerbare Loesung waere jetzt wohl,
statt des "schönen" std::result_of das "haessliche" (naja, das ist Gewohnheitsfrage) auto/decltype zu nehmen (sobald der Compiler mitspielt):

template<typename U, typename ... Args>
auto forward(U u, Args&&...args)
-> decltype( ((*(this->m)).*u)(std::forward<Args>(args)...) )
{
return ((*m).*u)(std::forward<Args>(args)...);
}

Da brauche ich dann nur vom return-Ausdruck abzuschreiben.

Helmut

Daniel Krügler

unread,
Apr 16, 2012, 1:00:48 PM4/16/12
to
Am Montag, 16. April 2012 07:19:59 UTC+2 schrieb Helmut Zeisel:
> Am Sonntag, 15. April 2012 16:46:05 UTC+2 schrieb Daniel Krügler:
>
> > Ich hoffe, die Sache ist etwas klarer geworden.
>
> Eigentlich bin ich jetzt noch mehr verwirrt ...
>
> Die am einfachsten erklaerbare Loesung waere jetzt wohl,
> statt des "schönen" std::result_of das "haessliche" (naja, das ist Gewohnheitsfrage) auto/decltype zu nehmen (sobald der Compiler mitspielt):
>

Eigentlich nicht schwierig: Man setzt für jeden lvalue den Typ per lvalue-Referenz und für jeden rvalue per rvalue-Referenz. Ist doch einfach, oder?

Daniel Krügler

unread,
Apr 16, 2012, 1:07:01 PM4/16/12
to
Am Montag, 16. April 2012 14:22:39 UTC+2 schrieb Helmut Zeisel:
> On Thursday, April 12, 2012 11:18:28 PM UTC+2, Daniel Krügler wrote:
>
> > aber gcc akzeptiert den Bezug auf "this->m" nicht. Dies lässt sich aber
> > beheben durch Verwendung von std::declval aus <utility>, womit wir uns einen entsprechenden Wert "erzeugen":
> >
> > template<typename U, typename ... Args>
> > auto forward(U u, Args ...args) ->
> > decltype( ((*(std::declval<std::shared_ptr<T>&>())).*u)(args...) )
> > {
> > return ((*m).*u)(args...);
> > }
>
> auto forward(U u, Args ...args) ->
> decltype( (std::declval<T>().*u)(args...) )

Korrekt wäre

auto forward(U u, Args ...args) ->
decltype( (std::declval<T&>().*u)(args...) )

da T als lvalue eingeht, nicht als rvalue.

> bzw. besser
>
> auto forward(U u, Args&& ...args) ->
> decltype( (std::declval<T>().*u)(std::forward<Args>(args)...) )

Auch hier gilt:

auto forward(U u, Args&& ...args) ->
decltype( (std::declval<T&>().*u)(std::forward<Args>(args)...) )

> klappt auch - gibt's da einen subtilen Unterschied zwischen
> *(std::declval<std::shared_ptr<T>&>()) und std::declval<T>() ?

Wenn du *(std::declval<std::shared_ptr<T>&>()) mit std::declval<T&>() hätte ich gesagt "Nein, kein Unterschied": In beiden Fällen liefert der Ausdruck ja einen lvalue vom Typ T. Der Unterschied ist, dass std::declval<T>() einen rvalue liefert. Auch hier gilt die gleiche einfache Regel wie bei result_of: Verwende eine lvalue-Referenz, wenn du einen lvalue meinst, anderenfalls entweder den Typ oder dessen rvalue-Referenz.

Besten Gruss aus Bremen,

Daniel Krügler

Helmut Zeisel

unread,
Apr 16, 2012, 2:05:31 PM4/16/12
to
Am Montag, 16. April 2012 19:07:01 UTC+2 schrieb Daniel Krügler:
> Am Montag, 16. April 2012 14:22:39 UTC+2 schrieb Helmut Zeisel:

> > auto forward(U u, Args ...args) ->
> > decltype( (std::declval<T>().*u)(args...) )
>
> Korrekt wäre
>
> auto forward(U u, Args ...args) ->
> decltype( (std::declval<T&>().*u)(args...) )
>
> da T als lvalue eingeht, nicht als rvalue.

Ok, wenn man das so betrachtet, ist es logisch.

Gibt es aber wirklich ein Beispiel, bei dem sich

decltype( (std::declval<T>().*u)(args...) )

von

decltype( (std::declval<T&>().*u)(args...) )

unterscheidet?

Helmut

Daniel Krügler

unread,
Apr 17, 2012, 1:36:10 PM4/17/12
to
Am Montag, 16. April 2012 20:05:31 UTC+2 schrieb Helmut Zeisel:
> Gibt es aber wirklich ein Beispiel, bei dem sich
>
> decltype( (std::declval<T>().*u)(args...) )
>
> von
>
> decltype( (std::declval<T&>().*u)(args...) )
>
> unterscheidet?

Sicher, habe ich auch schon vorher beschrieben. Hier ein Beispiel:

struct R {
void f() &&;
};

struct L {
void f() &;
};

Die Funktion R::f() ist *nur* durch einen rvalue von R aufrufbar, d.h. wenn u den Wert &R::f annimmt, ist

decltype( (std::declval<T>().*u)(args...) )

wohlgeformt, aber

decltype( (std::declval<T&>().*u)(args...) )

wird vom Compiler abgelehnt.

Andersherum ist die Funktion L::f() *nur* durch einen lvalue von L aufrufbar, de.h. wenn u den Wert &L::f annimmt, ist

decltype( (std::declval<T&>().*u)(args...) )

wohlgeformt, aber

decltype( (std::declval<T>().*u)(args...) )

wird vom Compiler abgelehnt.

Es liegt lediglich an der obskuren Regel in "altem" C++, dass eine Elementfunktion sowohl von einem rvalue *als* auch von einem lvalue von "this" aufgerufen werden kann. C++11 stellt die obengenannten ref-qualifizierten Element-Funktionen zur Verfügung. Sie sind aber in gcc derzeit noch nicht verfügbar.

Helmut Zeisel

unread,
Apr 17, 2012, 2:25:45 PM4/17/12
to
Am Dienstag, 17. April 2012 19:36:10 UTC+2 schrieb Daniel Krügler:

> Es liegt lediglich an der obskuren Regel in "altem" C++, dass eine
> Elementfunktion sowohl von einem rvalue *als* auch von einem lvalue von "this"
> aufgerufen werden kann. C++11 stellt die obengenannten ref-qualifizierten
> Element-Funktionen zur Verfügung.

Naja, momentan halte ich die noch die neue Reglen fuer obskurer. Aber langsam lichtet sich der Nebel.

Helmut

Daniel Krügler

unread,
Apr 18, 2012, 2:43:06 PM4/18/12
to
Die alte Regel *ist* obskur, aber leider unfixbar, weil zuviel Code existiert, der ihn verwendet. Das fällt insbesondere auf, wenn man den sog. "impliziten Objektparameter" einer Elementfunktion vergleicht mit anderen Referenz-Parametern (Laut Sprache verhält er sich genau wie ein Referenzparameter vom Typ des dazugehörenden Klassentyps). Beaschte, dass folgender Code gültig ist:

struct S {
void foo();
};

int main() {
S().foo(); // Erzeuge rvalue von S und rufe foo() auf
}

Wenn man sich das genauer anschaut, verhält sich dieser Aufruf analog zu einem "umgeschriebenen" Aufruf einer freien Funktion, bei der der erste Parameter eine Referenz auf ein nicht-konstantes Objekt von S ist, also so:

void foo(S&);

Versuche nunmal, diese Funktion mit einem rvalue von S aufzurufen:

int main() {
foo(S());
}

Das ist *unzulässig*, da dies ein rvalue an eine lvalue-Referenz binden würde. Leider gibt es noch ein paar wenige Compiler, die diesen Code akzeptieren. Falls deiner dazu gehört: Das ist kein gültiges C++.

Durch die Einführung von ref-qualifizierten Elementfunktionen hat man dem Entwickler ein Mittel an die Hand gegeben, an geeigneten Stellen diese Inkonsistenz aufzulösen. Deklariere ich eine Element-Funktion mit & ref-Qualifizierer, d.h. so:

struct S {
void bar() &;
};

dann ist der Objekt-Parameter ein echter lvalue-Referenz-Parameter. Sie verhält sich also (In bezug auf den Objektparameter) exakt wie eine freie Funktion mit dieser Deklaration:

void bar(S&);

Das wiederum bedeutet, das aus diesem Grund die obengezeigte Konstruktion

int main() {
S().bar(); // Fehler!
}

nicht kompiliert, aus dem gleiche Grund, warum der Ausdruck

bar(S())

nicht kompiliert. Analog ist das bei einer Deklaration mit einem && ref-Qualifizierer:

struct S {
void bar() &&;
};

Man braucht wenig geistige Vorstellungskraft zu verstehen, dass sich diese Funktion verhält wie das folgende freie-Funktions-Analogon:

void bar(S&&);

Damit ist, klar, dass es sich auf jeden Fall über einen rvalue aufrufen lässt, also ist das hier gültig:

int main() {
S().bar(); // OK!
}

Und aus dem gleichen Grund ist es unzulässig, diese Funktion mit einem lvalue von S aufzurufen:

int main() {
S s;
s.bar(); // Fehler!
}

Man kann also vollkommen analog zu freien Funktionen verstehen, welche Argumente zu welchen Funktionssignaturen passen.

Ich hoffe, das beseitigt das geistige Chaos etwas.
0 new messages