friend und namespaces

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Jonathan
Establishment
Beiträge: 2660
Registriert: 04.08.2004, 20:06
Kontaktdaten:

friend und namespaces

Beitrag von Jonathan »

Hallo,

ich habe meinen Zuweisungsoperator folgendermaßen implementiert:

Code: Alles auswählen

Channel& Channel::operator= (const Channel& rhs)
{
	if(this!=&rhs)
	{
		Channel tmp(rhs);
		swap(tmp, *this);
	}
	return *this;
}
Die Vorlage habe ich so in einem C++ Forum gefunden und es erscheint mir sehr hübsch, sowohl den Cpy-Ctor als auch den Destruktor wiederzuverwenden, um das alte Objekt nicht löschen zu müssen und das übergebene nicht kopieren zu müssen.

Als swap-Funktion möchte ich std::swap benutzen, allerdings in einer eigenen Spezialisierung, da ja sonst intern wieder operator= benutzt würde. Diese Spezialisierung soll nun ein friend von Channel sein und dann die Attribute jeweils einzeln swappen. Nur jetzt befürchte ich, dass mir die Namespaces irgendwie dazwischen kommen. Aussehen tut das ganze so (auf das wesentliche Reduziert):

Code: Alles auswählen

//hpp
namespace msi{

class Channel
{
	friend void ::std::swap(::msi::Channel& a, ::msi::Channel& b);
};

}
namespace std{ void swap(msi::Channel& a, msi::Channel& b); }

//cpp
namespace msi
{
Channel& Channel::operator= (const Channel& rhs){}//s.o.
}

namespace std
{
//perform a elementwise swap (instead of using =)
void swap(msi::Channel& a, msi::Channel& b)
{
	swap(a.m_Image, b.m_Image);
	swap(a.m_Id, b.m_Id);
	swap(a.m_ImageData, b.m_ImageData);
	swap(a.m_Width, b.m_Width);
	swap(a.m_Height, b.m_Height);
}
}
Das Problem ist, meine swap Funktion darf nicht auf die Attribute zugreifen, da diese private sind. Meiner Meinung nach müsste das daran liegen, dass die friend-Deklaration nicht mit der Funktionsdeklaration übereinstimmt (daher auch die wirklich expliziten namespace-Angaben in der friend Deklaration). Aber ich sehe einfach nicht, wo sie sich unterscheiden.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: friend und namespaces

Beitrag von CodingCat »

Da sind einige Fehler drin, aber zuerst zu deinem konkreten Problem: Wenn die befreundete Funktion nicht im gleichen Namespace liegt, muss sie bereits vor der friend-Deklaration deklariert worden sein. Bei dir wird sie wie es aussieht erst hinterher deklariert, wodurch deine friend-Deklaration vermutlich einfach auf die generische Standard-swap-Funktion aus dem std-Namespace angewandt wird.

Das was du da hast ist KEINE Spezialisierung, sondern eine Überladung. Dem std-Namespace Überladungen hinzuzufügen ist verboten und das ist auch gut so, weil vollkommen unnötig. Argument-Dependent Lookup sucht bei Funktionsaufrufen ohne explizite Namespace-Angabe automatisch immer zuerst in den Namespaces der Argumenttypen. Richtig wäre also, deine swap-Überladung einfach im msi-Namespace zu definieren. Dort wird sie dann auch von der STL gefunden. Argument-Dependent Lookup von generischen std-Funktionen außerhalb des std-Namespaces macht man übrigens mit using:

Code: Alles auswählen

template <class T>
void genericFoo(T &a, T &b)
{
   using std::swap;
   swap(a, b); // ADL findet für ALLE T die richtige Überladung
}
Jonathan hat geschrieben:ich habe meinen Zuweisungsoperator folgendermaßen implementiert:

Code: Alles auswählen

Channel& Channel::operator= (const Channel& rhs)
{
        if(this!=&rhs)
        {
                Channel tmp(rhs);
                swap(tmp, *this);
        }
        return *this;
}
Die Vorlage habe ich so in einem C++ Forum gefunden und es erscheint mir sehr hübsch, sowohl den Cpy-Ctor als auch den Destruktor wiederzuverwenden, um das alte Objekt nicht löschen zu müssen und das übergebene nicht kopieren zu müssen.
Die Vermeidung der Kopie ist Blödsinn, du kopierst doch ohnehin. Sinnvoller wäre:

Code: Alles auswählen

Channel& Channel::operator= (Channel rhs)
{
        swap(rhs, *this);
        return *this;
}
Damit profitierst du automatisch von Move-Semantics, sollte das übergebene Objekt ein R-Value sein. Die Funktion ist nicht nur einfacher zu lesen, sondern auch einfacher für den Compiler zu verarbeiten, z.B. für weitere Optimierung durch Copy Elision.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Jonathan
Establishment
Beiträge: 2660
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: friend und namespaces

Beitrag von Jonathan »

Habe gerade an dem Projekt weiter gearbeitet, vielen Dank. Ich glaube, das mit ADL sollte ich mir nochmal in Ruhe ansehen.

Kurze Frage: Deine Version des Zuweisungsoperators hat den Vorteil, dass die Kopie immer ausgeführt wird, was es dem Compiler leichter macht, das ganze zu optimieren? Allerdings würde bei Selbstzuweisung eine unnötige Kopie ausgeführt werden (jedoch ohne die Probleme, die eine einfache Selbstzuweisung mit sich bringen würde?).

Und: Ist der Zuweisungsoperator jetzt so, wie man ihn in der Regel implementieren würde? Oder war das einfach eine korrigierte Version der Variante, die ich gefunden habe?
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: friend und namespaces

Beitrag von CodingCat »

Die gezeigte Version hat den Vorteil, dass die Kopie auf Aufruferseite durchgeführt wird, wodurch du automatisch von Move Semantics profitierst.

Schlussendlich musst du dich fragen, wie oft eine Selbstzuweisung vorkommt. Ich halte es hier in der Regel so, dass ich Selbstzuweisungen als Randfall betrachte, den ich ausschließlich zur Sicherheit abdecke. Abgesehen von Hacks wie a = (x) ? a : b; fällt mir gerade kein normaler Fall ein, in dem eine Selbstzuweisung überhaupt regelmäßig auftreten könnte. Für einen Fall zu optimieren, der praktisch nie auftritt, halte ich für Unsinn, insbesondere wenn der Normalfall darunter leidet.

Wenn die Kopie extrem teuer ist, kannst du natürlich auch anders argumentieren und die zusätzliche if-Abfrage im Regelfall mit der gesparten teuren Kopie im Einzelfall amortisieren. Ob sich das lohnt, musst du im Kontext deiner Anwendung abschätzen.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Jonathan
Establishment
Beiträge: 2660
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: friend und namespaces

Beitrag von Jonathan »

Ich habe nochmal eine ähnliche Frage:

Wieder aufs Wesentliche gekürzt:

Code: Alles auswählen


template<typename t> class GpuMemory : public std::vector<t>
{
  template<typename b> friend void swap(GpuMemory<b>& r, GpuMemory<b>& l);
public:
  GpuMemory(const GpuMemory<t>& val);
  GpuMemory<t>& operator= (GpuMemory<t> val);
};

template<typename b> void swap(GpuMemory<b>& r, GpuMemory<b>& l)
{
  swap<std::vector<t>>(r, l);//swap the base object
  swap(r.m_GpuPointer, l.m_GpuPointer);// and our 2 variables
  swap(r.m_LastAllocatedSize, l.m_LastAllocatedSize);
}

template<typename t> GpuMemory<t>& GpuMemory<t>::operator= (GpuMemory<t> val)	
{
  swap(val, *this);
  return *this;
}
Das Hauptproblem, meine swap-Funktion wird nicht gefunden " 'std::vector<_Ty>::swap': Keine überladene Funktion akzeptiert 2 Argumente". Und dann bin ich mir bei der swap-Implementierung etwas unsicher, ob ich das mit der Basisklasse so korrekt gemacht habe. Meine Idee war, dass dieser Aufruf zunächst nur den std::vector swapt und ich danach noch meine 2 zusätzlichen Attribute swappen muss und damit dann mein gesamtes Objekt gewsapt habe. Aber ob die Schreibweise so stimmt, weiß ich nicht.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: friend und namespaces

Beitrag von CodingCat »

swap<std::vector<t>>(r, l); ist falsch. Gäbe es eine solche Spezialisierung tatsächlich, so müsstest du zumindest std:: davorschreiben, um nicht deine eigene Swap-Funktion rekursiv mit GpuMemory<std::vector<std::vector<std::vector<...>>>> aufzurufen. Tatsächlich vermeidet man Funktionsspezialisierungen allerdings nach Möglichkeit komplett, weil Überladungen dieselbe Funktionalität intuitiver und konsistenter bieten.

Hast du zwei Vektoren l und r, dann findet ein unqualifizertes swap(l, r) per Argument-dependent Lookup von ganz alleine die richtige Überladung im std-Namespace. In deinem Beispiel müsstest du zur Vermeidung von Rekursion allerdings l/r von Hand in reine Vektoren casten: swap(static_cast<std::vector<b>&>(l) , static_cast<std::vector<b>&>(r)). Alternativ könntest du std::swap(l, r) aufrufen, weil du den Namespace der Vektorklasse in diesem Fall kennst und er nicht mit deinem aktuellen Namensraum überlappt.

Die STL bietet neben der freien Swap-Funktion in der Regel auch eine Objektfunktion an, so kommen die Klassen einerseits ohne friend aus und vereinfachen andererseits die Notation bei Komposition und Vererbung:

Code: Alles auswählen

// std::vector-Vererbung aus Frage-Beitrag übernommen, NICHT EMPFOHLEN
template<typename t> class GpuMemory : public std::vector<t>
{
   typedef std::vector<t> base_type;
public:
   void swap(GpuMemory<b> &r)
   {
      using std::swap;
      this->base_type::swap(r);
      swap(m_GpuPointer, r.m_GpuPointer);
      swap(m_LastAllocatedSize, r.m_LastAllocatedSize);
   }
};

template<typename b> void swap(GpuMemory<b>& l, GpuMemory<b>& r) { l.swap(r); }
Der gezeigte Code ist als generisches Beispiel für den Umgang mit Swap in Vererbungshierarchien gedacht, von einer Vererbungsbeziehung mit Containerklassen rate ich grundsätzlich ab. Dasselbe lässt sich ohne Funktionalitätseinbußen wesentlich klarer durch einfache öffentliche Komposition umsetzen. Dann bist du auf einen Schlag die ganzen Vererbungsverwirrungen los:

Code: Alles auswählen

template<typename t> class GpuMemory
{
   ...
public:
   typedef std::vector<t> data_type;
   data_type data;

   void swap(GpuMemory<b> &r)
   {
      using std::swap;
      swap(data, r.data);
      swap(m_GpuPointer, r.m_GpuPointer);
      swap(m_LastAllocatedSize, r.m_LastAllocatedSize);
   }
};

template<typename b> void swap(GpuMemory<b>& l, GpuMemory<b>& r) { l.swap(r); }

// Anwendercode nutzt für Vektorfunktionalität jetzt gpumemory.data.push_back/resize/... statt gpumemory.push_back/resize/...
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Jonathan
Establishment
Beiträge: 2660
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: friend und namespaces

Beitrag von Jonathan »

Danke, jetzt kompilierts. Ich musste allerdings noch im Zuweisungsoperator ::swap(val, *this); benutzen, da er sonst die angesprochene Objektfunktion benutzen wollte, die nur einen Parameter erwartet, was die Fehlermeldung erklärt.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Antworten