[C++] Return reference naar stack waarde: mag dit?

Pagina: 1
Acties:

  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Om maar even met de deur is huis te vallen: als ik dit doe,
C++:
1
2
3
4
5
const SomeClass &SomeFunction( ... )
{
 ...
 return SomeClass( ... );
}


... geeft mijn compiler (GCC3) een warning, nl. dat ik een reference naar een local teruggeef.
Het werkt echter wel.

De reden is dat ik wil doen, is omdat het idee krijg dat bij het returnen van een value, een nieuwe instance met deze waarde wordt gealloceerd. Dit blijkt nl. uit het feit dat...
C++:
1
2
3
4
5
AbstractClass SomeFunction( )
{
 ...
 return DerivedClass( );
}

... niet mag.

Dus mijn vraag: is het bovenste voorbeeld legaal volgens de C++ standaard?

  • André
  • Registratie: Maart 2002
  • Laatst online: 06-05 11:13

André

Analytics dude


  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Oeps, had ik /13 ipv. /14 ... sorry & bedankt :o

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

wanneer je
C++:
1
2
3
4
5
6
7
8
someClass foo()
{
  //...
  return someClass(...);
}


someClass iets = foo(); // zie verder

doet wordt eerst een someClass instantie aangemaakt op de stack en daarna wordt dmv de COPY-constructor een klasse teruggegeven via de stack.

in jouw geval (met &) wordt echter een referentie naar het object in de lokale stack-space van de functie gegeven, maar die wordt "ongeldig" verklaard wanneer je return doet.

dus gooi die & weg. een beetje compiler optimaliseert dat returnen nl zelfs weg zodat het
C++:
1
2
3
4
5
6
7
void foo(someClass &returnwaarde)
{
  // doe iets met returnwaarde
}

someClass iets();
foo(iets);

wordt

[ Voor 3% gewijzigd door H!GHGuY op 18-06-2005 20:26 . Reden: const toekennen aan niet-const is niet netjes :o ]

ASSUME makes an ASS out of U and ME


  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Ok, ik snap wat je bedoelt.
Nou lag mijn prob. eigenlijk net iets moeilijker: ik wilde nl. een set non-member operators maken voor een abstracte class die gelijk konden gelden voor alle derived classen (hoop dat je me nu nog kan volgen :) )

Hier een stukje code:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
inline Vertex operator+( const Vertex& v1, const Vertex& v2 )
    {
        if( v1.is2D( ) || v2.is2D( ) )
        {
            Vertex2D v2d(
                v1.x( ) + v2.x( ),
                v1.y( ) + v2.y( ) );
            return v2d;
        }
        else if( v1.is3D( ) || v2.is3D( ) )
        {
            Vertex3D v3d(
                v1.x( ) + v2.x( ),
                v1.y( ) + v2.y( ),
                v1.z( ) + v2.z( ) );
            return v3d;
        }
        Vertex4D v4d(
            v1.x( ) + v2.x( ),
            v1.y( ) + v2.y( ),
            v1.z( ) + v2.z( ),
            v1.w( ) + v2.w( ) );
        return v4d;
    }

En zo ook voor de operator -, * en /.

Dit werkt niet omdat Vertex abstract is en er dus geen instance van gemaakt kan worden.
Vertex2D, 3D & 4D zijn dus Vertex-classen die slechts verschillen met Vertex in het feit dat ze de eigenlijke data (een float-array) bevatten.
Op deze manier kunnen alle drie deze typen op dezelfde wijze worden benaderd terwijl ze geen bit data méér bevatten dan werkelijk nodig is.
De z() & w( ) functie's voor een Vertex2D returnen bijvoorbeeld gewoon 0.0.

Naja, ik kan dit probleem ook makkelijk oplossen door voor alle VertexnD classen gewoon aparte binairy operators te maken en de base-class uit te rusten met een paar casting-operators.
:P

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
Whaaa! Hoe kom je daar nu weer op! Wat dacht je ervan om die operator voor verschillende klassen te overloaden, in plaats van één methode te maken die drie verschillende implementaties bevat?

Sowieso moet je nog even goed naar je klassestructuur kijken: wat hebben die klassen gemeen dat ze een gemeenschappelijke base Vertex hebben? Kun je niet beter een paar conversie-operators schrijven en alleen de afzonderlijke klassen laten bestaan die op zichzelf al bestaansrecht hebben?

Verder zou je kunnen overwegen om een overloaded template class te maken, zodat je de code voor 2D, 3D en 4D (?) vertices niet hoeft te dupliceren. Maar dat maakt het misschien nodeloos ingewikkeld.

[ Voor 18% gewijzigd door Soultaker op 18-06-2005 22:13 ]


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Je klasse structuur klopt niet echt, maar ik denk dat je probleem is dat je dus geen concreet Vertex object kan retourneren, toch? En dat je om dat op te lossen dacht aan een reference te retourneren. Maar dat kan niet naar een lokaal iets zoals highguy als zei. De oplossing is dan om een nieuw object op de heap aan te maken, dus met new. Alleen krijg je dan memory leak problemen...dat zou je kunnen oplossen door smart pointers te retourneren. Zoals je al ziet gaat het geen bliksem snelle oplossing worden (voor een kleine class als vertex dan, het valt op zich nog best mee). Dit soort dingen kan je dan ook het best met templates implementeren. Sowieso is code als "is2D() is3D()" etc erg brak...Wat gaat er gebeuren als ik een Vertex5d toevoeg? Je kan ook nog eens naar double dispatch kijken als oefening.

  • curry684
  • Registratie: Juni 2000
  • Laatst online: 06-05 14:03

curry684

left part of the evil twins

HIGHGuY schreef op zaterdag 18 juni 2005 @ 20:20:
n jouw geval (met &) wordt echter een referentie naar het object in de lokale stack-space van de functie gegeven, maar die wordt "ongeldig" verklaard wanneer je return doet.
Niet helemaal correct. Het object staat echt nog wel op de stack en is zo geldig als wat. Stel je zit in een functie, dan ziet je stack er extreem versimplificeerd zo uit:
code:
1
2
Hoop meuk
--- (stack pointer) ---

Nu roep je de functie SomeFunction aan. Deze heeft een aantal lokale variabelen. Je stack ziet er bij het aanvangen van de functie zo uit:
code:
1
2
3
4
Hoop meuk
PC om op terug te keren na de functie
Lokale variabelen SomeFunction
--- (stack pointer) ---

Da's het idee achter een stack tenslotte, dat je er dingen op stapelt die je in volgorde er terug vanaf kunt halen (ik negeer expres de calling conventions bdw). Als je aan het einde van de functie een temporary meegeeft, zie je stack er bij het verlaten van de functie zo uit:
code:
1
2
3
4
5
Hoop meuk
PC om op terug te keren na de functie
Lokale variabelen SomeFunction
De temporary die je instantieert
--- (stack pointer) ---

In de volgende regel verlaat je de functie, en wordt de stack dus opgeruimd. Dit gebeurt enkel door de SP terug te zetten, er wordt niets overheen geschreven natuurlijk, dat zou waste of cpu cycles zijn. Na het terugzetten wordt de oude PC teruggehaald om daar verder met uitvoering verder te gaan. Oftewel je stack ziet er na het terugkeren zo uit:
code:
1
2
3
4
5
Hoop meuk
--- (stack pointer) ---
PC om op terug te keren na de functie
Lokale variabelen SomeFunction
De temporary die je instantieert

Een reference of pointer naar een temporary is dus nog wel degelijk geldig! Als jij vervolgens echter een integer declareert (dus op de stack zet) wordt daar 4 bytes voor ingeruimd. Deze overschrijft de PC dus die er nog op stond. Als je een object van 32 bytes lokaal declareert zal deze de lokale variabelen van SomeFunction en wellicht je temp object overschrijven. Het is dus afhankelijk van de caller hoe lang de reference nog wel geldig blijft!

Dus ja, het zal werken, maar het is natuurlijk zo illegaal als wat, dus deze compiler warning betekent dat je altijd wat zal moeten fixen om dit te voorkomen (heap declareren, parent object laten declareren, copy constructor gebruiken etc.). Probleem is dat het op runtime gewoon geldige code is, omdat de stack als geheel 'van jouw process is' en dus geen access violation of zo zal geven. Maar je bent afhankelijk van het noodlot of het wel of niet geldig blijft :)

Professionele website nodig?


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
curry: dat zijn allemaal platformafhankelijke aannames. Misschien mag je op mijn platform helemaal niet voorbij de stackpointer schrijven (in principe zou dat geheugen tenslotte gealloceerd kunnen zijn); dan heb je pech. Vanuit C++ gezien valt er niets zinnigs over te zeggen.

Op een IA-32 architectuur gaat het toch al direct mis zodra je een method call op het geretourneerde object probeert aan te roepen, omdat je dan de argumenten (en de PC) over het object heen pusht. Zelfs als je direct een kopie maakt gaat het nog fout omdat dan een copy constructor aangeroepen wordt die het object al sloopt voordat 'ie aan het maken van een kopie toekomt.

En als je toch objecten gaat kopiëren, dan kun je beter direct een (non-reference) object value retourneren!

[ Voor 11% gewijzigd door Soultaker op 19-06-2005 02:30 ]


  • curry684
  • Registratie: Juni 2000
  • Laatst online: 06-05 14:03

curry684

left part of the evil twins

Soultaker schreef op zondag 19 juni 2005 @ 02:28:
curry: dat zijn allemaal platformafhankelijke aannames. Misschien mag je op mijn platform helemaal niet voorbij de stackpointer schrijven (in principe zou dat geheugen tenslotte gealloceerd kunnen zijn); dan heb je pech. Vanuit C++ gezien valt er niets zinnigs over te zeggen.
Klopt, maar op x86 zoals bij de topicstarter gelden de aannames wel. Het gaat om de versimplificeerde weergave ;) Vandaar ook de genegeerde calling conventions etc.: ik geef enkel aan waarom de code wel werkt en dat eigenlijk enorm toevallig is :)
Op een IA-32 architectuur gaat het toch al direct mis zodra je een method call op het geretourneerde object probeert aan te roepen, omdat je dan de argumenten (en de PC) over het object heen pusht. Zelfs als je direct een kopie maakt gaat het nog fout omdat dan een copy constructor aangeroepen wordt die het object al sloopt voordat 'ie aan het maken van een kopie toekomt.
Niet per definitie: als SomeFunction zelf genoeg andere variabelen op de stack heeft kan de speelruimte groot genoeg zijn voordat ie de instance manglet.
En als je toch objecten gaat kopiëren, dan kun je beter direct een (non-reference) object value retourneren!
Dat zeg ik ;)

Professionele website nodig?


  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Eeh, bedankt iedereen voor al deze uitleg!
Op een aantal punten ben ik het wel eens met Soultaker en Zoijar, maar als je naar dit kijkt:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
class MyObject
{
 zetPositie( const Vertex& v )
 {
  positie =v.as3D( ) ;
 }

 Vertex3D positie;
};

Vertex2D( 1.0, 2.0 );
myObject.zetPositie( v );


In dit geval wordt er slechts één keer een conversie gemaakt, nl. van Vertex2D naar Vertex3D op regel 5.

Als ik geen abstracte base Vertex zou gebruiken, zou het zo worden:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
class MyObject
{
 zetPositie( const Vertex3D& v )
 {
  positite =v;
 }

 Vertex3D positie;
};

Vertex2D( 1.0, 2.0 );
myObject.zetPositie( v.as3D( ) );


In dit geval is er een conversie én een assignment: nl. eerst v.as3D( ) en daarna de toekenning van positie =v;
Mijn manier mag dan vrij omslachtig aandoen, maar het is IMHO wel de meest efficiënte.

BTW: @Soultaker een vertex met 4 coördinaten (x,y,z,w) wordt in OpenGL gebruikt om met w( ) het type coördinaat aan te geven. In dat geval wordt het geinterpreteerd als x/w, y/w, z/w; dit komt echter maar zelden voor.

[ Voor 13% gewijzigd door phaas op 19-06-2005 12:09 ]


  • Daos
  • Registratie: Oktober 2004
  • Niet online
Het is lelijk en onhandig dat je een x, y en z gebruikt. Je kan het beter in een array (of Vector) zetten.

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

curry684 schreef op zondag 19 juni 2005 @ 01:30:
...hele uitleg...
Dus ja, het zal werken, maar het is natuurlijk zo illegaal als wat, dus deze compiler warning betekent dat je altijd wat zal moeten fixen om dit te voorkomen (heap declareren, parent object laten declareren, copy constructor gebruiken etc.).
dat weet ik, maar met invalid bedoelde ik puur wat jij zei: het is illegaal... de data zal er idd wel staan, maar je kan er geen veronderstelling van maken... de stack ptr schuift op en daardoor is alles wat eronder/boven (afh van architectuur natuurlijk) ligt, niet zeker dat et er nog staat.

net zoals wanneer je free() doet op een pointer de data niet gewist wordt maar je er niet op kan rekenen dat ze er nog staat...

net zo dus bij die stack... het staat er misschien(waarschijnlijk) nog, maar reken er maar neit op

daarnaast:
ik las es ergens dat het system idle proces ongebruikt geheugen op 0 zet(?) en als dat waar is kan je wel eens pach hebben als veronderstelling maakt dat je geheugen na free er nog staat...

@TS:
waarom zijn pointers + cast dan geen optie ?
daarenboven kun je de = operator ook overloaden... zelfs voor andere types
C++:
1
2
3
4
5
class B;
class A
{
   A& operator=(const B& b) { //doe iets; return *this }
}

ASSUME makes an ASS out of U and ME


  • curry684
  • Registratie: Juni 2000
  • Laatst online: 06-05 14:03

curry684

left part of the evil twins

HIGHGuY schreef op zondag 19 juni 2005 @ 13:19:
daarnaast:
ik las es ergens dat het system idle proces ongebruikt geheugen op 0 zet(?) en als dat waar is kan je wel eens pach hebben als veronderstelling maakt dat je geheugen na free er nog staat...
Geen idee of System Idle Process dat doet... ik zou het knap pointless vinden, en sowieso is ongebruikt geheugen in Windows altijd met #deadbeef gevuld, niet met 0 ;) Maar de stack is bezit van jouw process space, daar mag en kan een extern proces helemaal niet komen op straffe van access violation/segfault.

Professionele website nodig?


  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Daos schreef op zondag 19 juni 2005 @ 13:03:
Het is lelijk en onhandig dat je een x, y en z gebruikt. Je kan het beter in een array (of Vector) zetten.
Intern slaan de classen het op als een float-array. Deze kan worden benaderd door
• Een operator (const) float*( )
• Een operator[]
• De [set]x,y,z,w( ) convenience functie's
Die laatste zijn dus slechts een extra voor het gemak.

@HIGHGuY: ik snap ff niet helemaal wat je bedoelt.... geoverloade operator= heb ik al, net als een set casting-operators maar het probleem is dat ik wil voorkomen dat het die float-array onnodig gekopiëerd wordt.
Deze Vertex* objecten moeten nl. ook dienst doen in bv. een mesh-structuur en met enkele duizenden vertices telt elke byte aan data natuurlijk.

[ Voor 29% gewijzigd door phaas op 19-06-2005 13:37 ]


  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

curry684 schreef op zondag 19 juni 2005 @ 13:26:
[...]

Geen idee of System Idle Process dat doet... ik zou het knap pointless vinden, en sowieso is ongebruikt geheugen in Windows altijd met #deadbeef gevuld, niet met 0 ;) Maar de stack is bezit van jouw process space, daar mag en kan een extern proces helemaal niet komen op straffe van access violation/segfault.
ik heb er ook m'n twijfels over, over die idle process... en idd niemand komt met z'n fikken aan mijn process space!! :+
phaas schreef op zondag 19 juni 2005 @ 13:34:
[...]
Intern slaan de classen het op als een float-array. Deze kan worden benaderd door
• Een operator (const) float*( )
• Een operator[]
• De [set]x,y,z,w( ) convenience functie's
Die laatste zijn dus slechts een extra voor het gemak.

@HIGHGuY: ik snap ff niet helemaal wat je bedoelt.... geoverloade operator= heb ik al, net als een set casting-operators maar het probleem is dat ik wil voorkomen dat het die float-array onnodig gekopiëerd wordt.
Deze Vertex* objecten moeten nl. ook dienst doen in bv. een mesh-structuur en met enkele duizenden vertices telt elke byte aan data natuurlijk.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
class MyObject
{
 zetPositie( const Vertex& v )
 {
  positie =v.as3D( ) ;
 }

 Vertex3D positie;
};

Vertex2D( 1.0, 2.0 );
myObject.zetPositie( v );

wordt
C++:
1
2
3
4
5
6
class MyObject
{
  __force_inline MyObject& operator=(const Vertex& v) { positie = v.as3D() }
}
MyObject blah();
blah = myVertex;

ik weet niet of dit binnen je programmacode logisch is, maar dit kan je mss wel wat besparen?

daarenboven: zolang je met pointers en references werkt wordt er helemaal nix gekopieerd behalve die ptrs...

om even terug te komen op je topicstart:
C++:
1
2
3
4
AbstractClass* someFunction(foo a, bar b)
{
  return new DerivedClass(a,b);
}

zal wel werken en is efficient in die zin dat er 1 constructor aageroepen wordt en tijdens de return enkel een ptr doorgespeeld wordt...
nauwgezet pointers bijhouden is de boodschap. daarenboven kan die 4(of 8)byte aan ptr in vergelijking met de grootte van je class 'veel' zijn, en dan kan je mss toch beter een andere manier zoeken

dit zal je wsch wel weten:
C++:
1
2
DerivedClass b();
AbstractClass a = b;

kopieert enkel de members van abstractclass binnen de nieuwe klasse in a...
dus je bent verplicht om pointers te gebruiken

ASSUME makes an ASS out of U and ME


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 02-05 01:32
phaas schreef op zondag 19 juni 2005 @ 12:05:
Op een aantal punten ben ik het wel eens met Soultaker en Zoijar, maar als je naar dit kijkt:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
class MyObject
{
 zetPositie( const Vertex& v )
 {
  positie =v.as3D( ) ;
 }

 Vertex3D positie;
};

Vertex2D( 1.0, 2.0 );
myObject.zetPositie( v );

In dit geval wordt er slechts één keer een conversie gemaakt, nl. van Vertex2D naar Vertex3D op regel 5.

Mijn manier mag dan vrij omslachtig aandoen, maar het is IMHO wel de meest efficiënte.
Je zou zetPositie() kunnen overloaden voor een argument van type Vector2D. Dat is toch een speciaal geval omdat je er dan een dimensie bij moet verzinnen en de implementatie is dan zelfs 'efficiënter' omdat als je geen objecten hoeft te kopiëren, maar efficientie is een beetje een beladen begrip in een taal als C++ waarin de compiler een hele trukendoos aan optimalisaties kan gebruiken.

Sowieso lijkt het mij logischer in plaats van die as3D()-methode een constructor voor Vector3D te maken die een Vector2D accepteert (en implementeer dan de conversie operator daar ook gelijk mee), al heeft jouw methode het voordeel dat 'ie de conversie expliciet maakt.

Heb je toevallig een Java-achtergrond? Je lijkt wel programmeerervaring te hebben, maar de typische C++-oplossingen (gebruik van overloaded methods/constructors, conversie operatoren) lijken niet direct bij je op te komen. ;)
BTW: @Soultaker een vertex met 4 coördinaten (x,y,z,w) wordt in OpenGL gebruikt om met w( ) het type coördinaat aan te geven. In dat geval wordt het geinterpreteerd als x/w, y/w, z/w; dit komt echter maar zelden voor.
Ah, ik wist wel dat dat in transformatiematrices gebruikt werd, maar ik wist niet dat het ook nuttig was voor losse vertices (die naar mijn idee gewoon een punt in de ruimte representeren).
curry684 schreef op zondag 19 juni 2005 @ 13:26:
sowieso is ongebruikt geheugen in Windows altijd met #deadbeef gevuld, niet met 0 ;)
Is dat niet alleen in debug mode? En is het dan niet de runtime library die dat doet, niet de Windows kernel?

[ Voor 8% gewijzigd door Soultaker op 19-06-2005 15:55 ]


  • phaas
  • Registratie: Augustus 2001
  • Laatst online: 23-01-2025
Soultaker schreef op zondag 19 juni 2005 @ 15:53:
[...]

Je zou zetPositie() kunnen overloaden voor een argument van type Vector2D. Dat is toch een speciaal geval omdat je er dan een dimensie bij moet verzinnen en de implementatie is dan zelfs 'efficiënter' omdat als je geen objecten hoeft te kopiëren, maar efficientie is een beetje een beladen begrip in een taal als C++ waarin de compiler een hele trukendoos aan optimalisaties kan gebruiken.

Sowieso lijkt het mij logischer in plaats van die as3D()-methode een constructor voor Vector3D te maken die een Vector2D accepteert (en implementeer dan de conversie operator daar ook gelijk mee), al heeft jouw methode het voordeel dat 'ie de conversie expliciet maakt.

Heb je toevallig een Java-achtergrond? Je lijkt wel programmeerervaring te hebben, maar de typische C++-oplossingen (gebruik van overloaded methods/constructors, conversie operatoren) lijken niet direct bij je op te komen. ;)
Haha, grappig dat je dat zegt. Ik heb geen ervaring met java maar wel ongv. drie jaar ervaring met C++.
Misschien dat mn benadering een beetje java-achtig aandoet omdat ik in het verleden (en nog regelmatig) veel met Qt heb gewerkt. Zij hebben er een handje van hun classen erg on-c++'ig aan te laten doen. Dat ga je helaas overnemen :)

Die as3D( ) methoden zijn een beetje overbodig idd, ik had nl. al een casting-operator en een copy-constructor.
Waarschijnlijk praten we gewoon een beetje langs elkaar heen, misschien maakt mn code het ea. duidelijk.
http://edukitty.org/~barr...ty/gritty/gritty/vertex.h

Ik kan bij het proggen altijd erg moeilijk elke allocatie gaan zitten tellen zonder dat ik eigenlijk rekening houdt met 'truukjes' die de compiler achter de schermen uitvoert. Dat is opzich wel een goede gewoonte maar er gaat wel een hoop tijd inzitten :)

  • curry684
  • Registratie: Juni 2000
  • Laatst online: 06-05 14:03

curry684

left part of the evil twins

Soultaker schreef op zondag 19 juni 2005 @ 15:53:
[...]

Is dat niet alleen in debug mode? En is het dan niet de runtime library die dat doet, niet de Windows kernel?
Echt geen idee eigenlijk, als iemand zin heeft om het op te zoeken hoe het er komt en wie het er neerzet ben ik eigenlijk best benieuwd :D

De #deadbeef lijkt me op zich een klassieke magic number voor integriteits- en overflowtests, dus op zich klinkt je verklaring logisch, maar ik geloof dat ik het ooit ook wel op random memory spots heb aangetroffen :)

Professionele website nodig?


  • The End
  • Registratie: Maart 2000
  • Laatst online: 21:13

The End

!Beginning

In Windows NT worden gebruikte (vrijgegeven) pages voor zover mogelijk tijdens de idle time op 0 gezet. Dit was volgens mij een requirement van defensie in de VS ivm met bescherming van gevoelige informatie.

Er draait een thread met de laagste prioriteit die de pages op 0 zet (Zal wel niet in het system idle process zijn). Als er direct pages nodig zijn en er zijn geen gezerode pages beschikbaar, dan worden ze eerst op 0 gezet en daarna gebruikt.
Pagina: 1