Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

1

23.03.2008, 15:07

Referencecounted Strings

1.0 Vorwort
Da Zeichenketten ein essentielles Element von Programmen sind existieren viele Klassen die verschiedene Funktionalitäten zur Zeichenkettenmanipulation kapseln. Thema dieses Tutorials ist es nicht eine weitere Stringklasse zu erstellen, viel mehr will ich speziell auf ein Unterthema eingehen, das Kopierverhalten von Stringobjekten.
Beim kopieren von Zeichenketten kann man zwischen zwei Verhalten unterscheiden. Zum einen das duplizieren der kompletten Objektresourcen, beim erstellen einer Kopie des Strings. Zum anderen das zählen von Referenzen auf die Ressourcen. Genau letzteres Verhalten soll Thema dieses Tutorials sein.

2.0 Referenzzählung
Um den Begriff nochmals kurz zu erläutern. Das zählen von Referenzen auf ein Objekt (engl. Reference counting) ermöglicht es zum einen eine Art Buchhaltung zu führen ob ein Objekt noch benötigt wird, oder nicht. Dies hat einige Vorteile, z.B. ist es möglich zu entscheiden ob ein Objekt gelöscht werden soll (Smartpointer) oder um allgemein festzustellen ob noch andere Referenzen auf ein Objekt existieren.
Es gibt einige Ansätze um dieses Pattern zu implementieren. Es ist z.B. möglich eine abstrakte Klasse für zählbare Objekte zu erzeugen und konkrete Implementationen an einem Smartpointer zu übergeben. Ein anderer Ansatz ist es den Zähler als geteilten Speicher (engl. shared memory) auf dem Heap direkt im Smartpointer zu halten. Jede der bekannten Techniken hat ihre Vor- und Nachteile, so erlaubt z.B. die zuerst genannte keine primitiven Datentypen als zählbare Objekte, letztere ist u.U. schwerer zu implementieren. Beide genannten Techniken finden aber Verwendung, so beruht die Smartpointer Implementierung von boost z.B. auf dem zuletzt genannten Ansatz.
Weitere Informationen zu dem Thema finden sich u. A. in Scott Meyers Buch „More effective c++“.

3.0 Reference counted Strings
Eins der Einsatzgebiete von Referenzzählung ist z.B. das zählen von Referenzen auf Stringobjekte. Es kommt nicht selten vor das ein Programm viele Zeichenkennten nutzt die sich exakt gleichen. Es entstehen oftmals (temporäre-) Kopien von Zeichenketten, und jede Kopie belegt gleich viel Speicher, nämlich im besten Fall genau ‚n‘ mal die Anzahl der Zeichen im String.
Besser wäre es aber wenn Strings, deren Inhalt sich genau gleicht, auf denselben Speicher zeigen, und einfach Buchhaltung über die Anzahl der Referenzen führen würden. Nach kurzen nachdenken wird man aber feststellen das, sobald zwei Strings sich speicher Teilen, eine Änderung an einem der Strings nicht mehr möglich ist, da die Änderung sich ja auf beide Objekte beziehen würde. Und genau das bringt uns zum nächsten Abschnitt, dem…

4.0 Lazy loading
Unter lazy loading versteht man ein Designpattern das es erlaubt Ressourcen spät möglichst zu laden (bzw, hier COW (copy on write)). Der späteste Zeitpunkt für ein Stringobjekt ist es, wenn das Objekt geändert werden soll. Das Vorgehen, speziell bei Strings, ist folgendermaßen.

• Prüfen ob andere Referenzen existieren
• Ist dies der Fall müssen die Ressourcen geladen, und der Referenzzähler auf eins gesetzt werden
• Andernfalls muss nichts passieren
Ist das Objekt „einzigartig“ können die Daten manipuliert werden ohne andere Objekte zu beeinflussen.

5.0 Designideen
Nun zum Praktischen Teil der trockenen Theorie.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template< typename T >
class TString
{
class StringBuffer
    {
        ~StringBuffer()
        {
            delete [] m_pData;
        }

        uint32 m_nAllocated;
        uint32 m_nLen;
        T* m_pData;
    };

private:
    SharedPtr< StringBuffer > m_pBuffer;
};


In diesem Beispiel werden die tatsächlichen Daten von einem Objekt vom Typ StringBuffer gehalten. Der String selbst hält nur einen Zeiger auf die Daten. Dies ermöglicht es gleiche Zeichenketten von mehreren Stringobjekten zu referenzieren. Theoretisch ist das Kopierverhalten des Strings schon korrekt, wäre da nicht die Problematik, welche in Abschnitt 4.0 besprochen wurde, dem späten laden von Ressourcen. Es muss also eine Möglichkeit geben ein Objekt aus der Referenzzählung abzukoppeln. Um die Kopie der Daten zu erzeugen bietet sich ein Kopierkonstruktor von StringBuffer an.

C-/C++-Quelltext

1
2
3
4
5
6
7
StringBuffer( const StringBuffer& other ) : m_nAllocated( 0 ), m_nLen( 0 ), m_pData( NULL )
{
    Resize( other.m_nAllocated );
    TString< T >::_copy( m_pData, other.m_pData, other.m_nLen );
    m_nLen = other.m_nLen;
    m_pData[ m_nLen ] = 0;
}


Der Code ist selbsterklärend. Resize() vergrößert den Speicher und _copy() ist eine einfache Hilfsmethode um die Daten zu kopieren:

C-/C++-Quelltext

1
2
3
4
5
template< typename T >
inline void TString< T >::_copy( T* dest, const T* source, uint32 count )
{
    memcpy( dest, source, count * sizeof( T ) );
}

Jetzt sind alle nötigen Vorkehrungen getroffen um das Objekt abzukoppeln. Da dies an vielen Stellen passiert lohnt es sich auch hier eine Hilfsfunktion zu schreiben:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
template< typename T >
inline void TString< T >::_UniqueBuffer()
{
    if ( IsShared() )
    {
        SharedPtr<StringBuffer>(
new StringBuffer(*m_pBuffer)).Swap(m_pBuffer);
    }
}


Um ein korrekten „Reset“ des Zeigers zu gewährleisten kann, wie im obigen Code, das copy and swap Idiom verwendet werden. Es wird eine temporäre Kopie erzeugt anschließend die Inhalte der Kopie und dem eigentlichen Zeiger getauscht. Dieses Vorgehen hat u. A. auch den Vorteil das, beim zerstören der temporären Kopie, die Daten vom Smartpointer freigegeben werden, oder zumindest der Refernzzähler dekrementiert wird.
Bei vielen, aber nicht allen, Methoden wird es allerdings notwendig sein den Stringpuffer, nach dem Abkoppeln, noch zu vergrößern. Daher lohnt sich auch hier eine Hilfsmethode:

C-/C++-Quelltext

1
2
3
4
5
6
template< typename T >
inline void TString< T >::_ModifyContent( uint32 size )
{
    _UniqueBuffer();
    m_pBuffer->Resize( size );
}


Es sind nun alle Werkzeuge da um einige wichtige Stringfunktionen zu implementieren. Zum Beispiel das Zuweisen eines Strings.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
template< typename T >
inline void TString< T >::_Assign( const T* str, uint32 len )
{
    _ModifyContent( len );
    _copy( m_pBuffer->m_pData, str, len );
    m_pBuffer->m_pData[ len ] = 0;
    m_pBuffer->m_nLen = len;
}


Die Durchführung ist sehr einfach. Das Objekt wird, falls notwendig, entkoppelt, auf die richtige Größe gebracht und schließlich die Daten kopiert. Will man einen nullterminierten String halten so muss, konsequent bei jeder notwendigen Funktion, eine weitere 0 an das Ende der Daten gehängt werden.
Für jede Funktion welche die Daten der Zeichenkette ändert ist die grundlegende Vorbereitung gleich der, bei _Assign(), vorgestellten. Der Unterschied liegt nur bei der Bearbeitung der Daten. Es ist aber nun mehr möglich Stringobjekte auf geteilten Speicher zeigen zu lassen, bis die Daten geändert werden sollen. Und das kann schon eine große Ersparnis Ressourcen sein.

6.0 Ausnahmen
Um Langeweile vorzubeugen muss es natürlich Ausnahmen geben. Will man z.B. Referenzen auf Stringdaten bereitstellen bekommt man spätestens dann Probleme wenn der String wieder mehrfach referenziert wird. Ein Beispiel:

C-/C++-Quelltext

1
2
3
4
String str1 = "Hallo Welt";
char& c = str1[ 1 ];
String str2 = str1;
c = 'e';


Der []-Operator kommt mit seiner nicht konstante Version häufig mit Referenzen als Rückgabewert. Dies macht ja Sinn, da der Operator eine Manipulation von internen Daten bezwecken soll. In diesem Fall birgt die Idee aber Risiken, da das Beispiel zu viel des Guten tut und sich die Änderung gleich auf alle Referenzierenden Stringobjekte bezieht.
Dieses Verhalten ist in den meisten Fällen nicht gewünscht. Und so muss eine Abhilfe her, falls der Operator unbedingt bereitgestellt werden soll. Die Idee ist es, das Kopierverhalten des Referenzierten Objekts zu verändern, selbiges nämlich als nicht Teilbar zu kennzeichnen. Wird eine Kopie eines solchen Objekts erzeugt so werden die Daten tatsächlich kopiert und nicht einfach der Referenzzähler inkrementiert. Um dies zu erreichen muss die Funktion _UniqueBuffer() leicht geändert werden:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
template< typename T >
inline void TString< T >::_UniqueBuffer( bool makeUnshareable )
{
    if ( makeUnshareable || IsShared() )
    {
        SharedPtr< StringBuffer >( 
new StringBuffer( *m_pBuffer ) ).Swap( m_pBuffer );
        
        if ( makeUnshareable )
            ::MakeTargetUnshareable( m_pBuffer );
    }
}


Beim abkoppeln des Objekts kann das Objekt nun gekennzeichnet werden. Dies ist notwendig um später feststellen zu können ob das Objekt kopiert werden muss. Nun ist es möglich den []-Operator zu implementieren ohne die oben genannten Probleme zu bekommen.

C-/C++-Quelltext

1
2
3
4
5
6
7
template< typename T >
inline T& TString< T >::operator[]( uint32 index )
{
    assert( index < m_pBuffer->m_nLen );
    _UniqueBuffer( true );
    return m_pBuffer->m_pData[ index ];
}


Das Problem ist nun aber, dass das Objekt zwar als nicht „shareable“ gekennzeichnet ist, das Kopierverhalten selbst aber noch nicht geändert wurde. Betroffen sind zwei Stellen, nämlich der Kopierkonstruktor und der Zuweisungsoperator.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
template< typename T >
inline void TString< T >::_SharedAssign( const TString< T >& str )
{
    if ( !::IsTargetUnshareable( str.m_pBuffer ) )
        m_pBuffer = str.m_pBuffer;
    else
        SharedPtr< StringBuffer >( 
new StringBuffer( *str.m_pBuffer ) ).Swap( m_pBuffer );
}


Alles was getan werden muss ist zu überprüfen ob das Quellobjekt „teilbar“ ist. Ist dies der Fall kann wie gehabt fortgefahren werden. Ansonsten muss eine Kopie des Puffers erzeugt werden. Hiermit können Kopierkonstruktor und Zuweisungsoperator implementiert werden:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template< typename T >
inline TString< T >::TString( const TString< T >& str )
{
    Initialize();
    Assign( str );
}

template< typename T >
inline TString< T >& TString< T >::operator=( const TString< T >& str )
{
    if ( this != &str )
        _SharedAssign( str );

    return *this;
}

template< typename T >
inline TString< T >& TString< T >::Assign( const TString< T >& str )
{
    _SharedAssign( str );
    return *this;
}


7.0 Zusammenfassend
Ein Problem gibt es noch. Die Klasse StringBuffer ist kein zählbares Objekt. Die Funktionalität muss noch programmiert werden. Da es ja durchaus vorkommen Mag dass die Funktionalität an verschiedenen Stellen verwendet werden kann, lohnt es sich eine Basisklasse zu erzeugen, welche die grundlegende Funktionalität bereitstellt:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
template< typename T, T UnshareableTag >
class UnshareableRefTarget
{
public:
    typedef T ReferenceType;

    void AddReference( bool makeUnshareable = false )
    {
        if ( !makeUnshareable && m_nRefCount != UnshareableTag )
            ++m_nRefCount;
        else
            m_nRefCount = UnshareableTag;
    }

    void Release()
    {
        if ( m_nRefCount == UnshareableTag || --m_nRefCount <= 0 )
            delete this;
    }

    const T& References() const
    {
        return m_nRefCount;
    }

    void MakeUnshareable() 
    {
        m_nRefCount = UnshareableTag;
    }

    bool IsUnshareable() const
    {
        return m_nRefCount == UnshareableTag;
    }

protected:
    UnshareableRefTarget() : m_nRefCount( T() )
    {
    }

    UnshareableRefTarget( const UnshareableRefTarget& ) : m_nRefCount( T() )
    {
    }

    virtual ~UnshareableRefTarget()
    {
    }

    UnshareableRefTarget& operator=( const UnshareableRefTarget& )
    {
        return *this;
    }

private:
    T m_nRefCount;
};


Außerdem werde zwei Funktion benötigt, um feststellen zu können ob ein Objekt „teilbar“ ist oder nicht und um ein solches als „nicht teilbar“ zu markieren.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
template< typename Ptr >
inline void MakeTargetUnshareable( const Ptr& ptr )
{
    ptr.m_pPtr->MakeUnshareable();
}

template< typename Ptr >
inline bool IsTargetUnshareable( const Ptr& ptr )
{
    return ptr.m_pPtr->IsUnshareable();
}


Alles was nun noch getan werden muss ist die Klasse Stringbuffer als Spezialisierung von UnshareableRefTarget zu kennzeichnen:

C-/C++-Quelltext

1
class StringBuffer : public UnshareableRefTarget< int32, ( int32 )~0 >


8.0 Nachwort
Dieses Tutorial entstand nachdem ich in meinem Framework ein allgemeines String Template implementiert hatte. Da ich einiges an Zeit investiert habe um mich mit den bekannten (und nicht bekannten) Problemen zu befassen, habe ich beschlossen das Ganze mal zusammenzufassend als Tutorial zu schreiben. Ich hoffe das der Leser einige Informationen aus dem Text ziehen kann und hoffe auch das selbigen einiges an Informationssuche Aufwand erspart bleiben kann!
@D13_Dreinig

grek40

Alter Hase

Beiträge: 1 491

Wohnort: Dresden

  • Private Nachricht senden

2

23.03.2008, 17:47

Habs erstmal grob überflogen, sieht recht interessant aus. Werds mal im Hinterkopf behalten, damit ich wenn ich mal sowas brauch weiss wo es steht :)
So alltäglich nutzt man sowas ja dann doch nicht (bewusst).


Speziell zu dem [] operator is mir jetzt grad eingefallen, dass ich da mal ein ähnliches Problem in anderem Zusammenhang hatte - da hab ich dann eine kleine Hilfsklasse erstellt, in der Zugriffsindex, Herkunftsobjekt und das eigentlich von [] erwartete Item gespeichert waren. Per operator = und anderen Hilfsmitteln (weiss grad nich genau was es noch war) konnte ich dann genau festhalten, wenn tatsächlich eine Änderung an dem Element hinter [] durchgeführt wird und dann entsprechende zusätzliche Sachen erledigen, während ein Lesezugriff ohne Eingriffe durchgeführt wurde.

Pseudo:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Helper
{
  iArray _base;
  int _index, _value;

public:
  Helper(iArray* base, int index, int value)
    :_base(base), _index(index), _value(value)
  {}

  Helper operator =(int newval)
  {
    _base.Set(index, newval);
    return (*_base)[index];
  }

  operator int()
  {
    return _value;
  }
};

class iArray
{
  int* _array;

public:
  Helper operator[](int index)
  {
    return Helper(this, index, _array[index]);
  }

  void Set(int index, int newval)
  {
    // Aktionen für den Fall einer Änderung

    // ...

    _array[index] = newval;
  }
};


Ob das so sinnvoll(er) ist darüber kann man sich sicher streiten, aber es war halt der Ansatz den ich irgendwann mal gewählt hab für sowas und der Vorteil hier wäre ja, dass die Kopie nicht schon beim ersten Aufruf von operator[] gemacht werden muss, obwohl evtl. nur der Wert abgefragt und nicht geändert wird.

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

3

23.03.2008, 18:09

Die Idee kam mir auch, allerdings hab ich sie dann wieder aus irgendwelchen Gründen verworfen. Was macht dein Ansatz egtl wenn *_array neu allokiert wird, oder sich die Indice verschieben?
@D13_Dreinig

rewb0rn

Supermoderator

Beiträge: 2 773

Wohnort: Berlin

Beruf: Indie Game Dev

  • Private Nachricht senden

4

23.03.2008, 19:46

Sieht gut aus. Hab erst gestern den Artikel über referenzzählende Speicherverwaltungsobjekte in Effective C++ gelesen, da ist das ein guter Anschluss. Ich vertraue mal deinem Fachwissen dahingehend, dass das Tutorial ausgereift genug ist, um in das entsprechende Unterforum verschoben zu werden ;)

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

5

23.03.2008, 22:38

Zitat von »"rewb0rn"«

Sieht gut aus. Hab erst gestern den Artikel über referenzzählende Speicherverwaltungsobjekte in Effective C++ gelesen, da ist das ein guter Anschluss. Ich vertraue mal deinem Fachwissen dahingehend, dass das Tutorial ausgereift genug ist, um in das entsprechende Unterforum verschoben zu werden ;)


Danke für dein Vertrauen! ;)
@D13_Dreinig

grek40

Alter Hase

Beiträge: 1 491

Wohnort: Dresden

  • Private Nachricht senden

6

24.03.2008, 00:31

Zitat von »"David_pb"«

Die Idee kam mir auch, allerdings hab ich sie dann wieder aus irgendwelchen Gründen verworfen. Was macht dein Ansatz egtl wenn *_array neu allokiert wird, oder sich die Indice verschieben?

wann / wo soll das denn passieren? Helper existiert ja nur temporär als Rückgabewert -> entweder '_array' wurde vorher verändert oder es wird nachher verändert (Multithreatingsicher is das natürlich noch nicht so). Falls du meinst, dass in 'Set' eine komplette Veränderung stattfinden könnte... Die Veränderungen dürften eigentlich nur die Adresse von _array, nicht aber die Indizierung beeinflussen. Um flexibel auf Änderungen zu reagieren gebe ich ja mit

C-/C++-Quelltext

1
return (*_base)[index];
wieder nen neuen Helper zurück und nicht den alten.

//
Allerdings gibt es sicher auch Dinge, die dagegen sprechen, ich wollt es nur als Ansatz erwähnen, weil es halt noch mehr Möglichkeiten bietet, die Komplettkopie zu verzögern / vermeiden.

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

7

24.03.2008, 09:58

Es könnte aber z.B. passieren das, durch z.B. ein Insert, sich die Indice verschieben und der "Helper" auf den falschen Index referenziert.
@D13_Dreinig

grek40

Alter Hase

Beiträge: 1 491

Wohnort: Dresden

  • Private Nachricht senden

8

24.03.2008, 12:46

Hmm ja, paar Sachen fallen mir jetzt auch ein, die nicht so gut gingen...

War erstma davon ausgegangen, dass nur das Element direkt hinter dem Index angesprochen wird... hatte die (nicht ganz seltene) Herangehensweise 'fn(&string[x])' vergessen.
Im Bezug auf nen Insert is halt die Frage, ob man ne Zwischenspeicherung von Helperobjekten zulässt - weil nur dann kann sich ja der Index unbemerkt verschieben, ansonsten würde mich mal interessieren, wie so ein Aufruf von Insert, der den Helper praktisch unbrauchbar macht aussehen soll (vorausgesetzt der Helper wird genutzt als wenn der operator[] einfach nur ne Referenz auf ein arrayitem geben würde).

Helmut

5x Contest-Sieger

Beiträge: 692

Wohnort: Bielefeld

  • Private Nachricht senden

9

27.03.2008, 13:06

Man könnte die Klasse auch so erweitern, dass jeder String speichert, welcher Teil bom Buffer ihm eigentlich gehört, sodass eine Methode á la substr ohne Heapallokationen möglich ist. (Allerdings wird c_str dann wiederum schwierig)

In D haben übrigens alle Arrays diese Eigenschaft. Strings gibts da praktisch nicht, man kann einfach char[] schreiben und hat praktisch alle Eigentschaften dieser Klasse.

Allerdings denke ich, dass der ganze Aufwand für C++ Strings umsonst ist, da es in der Praxis keinen merkbaren Geschwindigkeitsvorteil bringen wird.

Ciao
Sei stets geduldig gegenüber Leuten, die nicht mit dir übereinstimmen. Sie haben ein Recht auf ihren Standpunkt - trotz ihrer lächerlichen Meinung. (F. Hollaender)

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

10

27.03.2008, 13:28

Zitat von »"Helmut"«

Man könnte die Klasse auch so erweitern, dass jeder String speichert, welcher Teil bom Buffer ihm eigentlich gehört, sodass eine Methode á la substr ohne Heapallokationen möglich ist. (Allerdings wird c_str dann wiederum schwierig)

In D haben übrigens alle Arrays diese Eigenschaft. Strings gibts da praktisch nicht, man kann einfach char[] schreiben und hat praktisch alle Eigentschaften dieser Klasse.

Allerdings denke ich, dass der ganze Aufwand für C++ Strings umsonst ist, da es in der Praxis keinen merkbaren Geschwindigkeitsvorteil bringen wird.

Ciao


Könnte man machen, aber das halte ich für ein wenig zu extrem. Vermutlich reist der ganze Managementaufwand den theoretischen "Vorteil" wieder runter.
Copy on Write allerdings kann durchaus einiges an Geschwindigkeitsvorteil bringen, schon um (teure) heap Allokationen zu vermeiden.
@D13_Dreinig

Werbeanzeige