Toon posts:

[c++] Nonconst references initializeren met temporaries

Pagina: 1
Acties:

Verwijderd

Topicstarter
In C++ kun je temporaries maken:
code:
1
2
struct S1 {};
void f1 () { S1(); }

Deze temporaries zijn niet const:
code:
1
2
struct S2 { void m (); };
void f2 () { S2().m(); }

Toch kun je nonconst references niet initializeren met een temporary:
code:
1
2
3
struct S3 { S3 (); private: S3 (S3 const &); };
void g1 (S3 &);
void f3 () { g1(S3()); /* errors */ }

De compiler geeft de volgende errors:
Initial value of reference to non-const must be an lvalue.
S3::S3(const S3 &) is inaccessible.
Mijn eerste vraag betreft de tweede error.
Kennelijk kan de nonconst reference wel geinitializeerd worden met een extra kopie van de temporary (dit is overigens niet wat ik wil en daarom heb ik de copy constructor even gedisabled). Waarom?

Een simpele werkende workaround is als volgt:
code:
1
2
3
struct S4 { S4 (); S4 & ref () { return *this; } private: S4 (S4 const &); };
void g2 (S4 &);
void f4 () { g2(S4().ref()); }

Mijn tweede vraag luidt als volgt: Als je met zo'n simpele workaround hetzelfde effect kan verkrijgen, wat is dan het nut van deze restrictie, en wat hebben lvalues hiermee te maken?

Verwijderd

Verwijderd schreef op 11 augustus 2002 @ 17:45:
Mijn eerste vraag betreft de tweede error.
Kennelijk kan de nonconst reference wel geinitializeerd worden met een extra kopie van de temporary (dit is overigens niet wat ik wil en daarom heb ik de copy constructor even gedisabled). Waarom?
Je kunt een non-referenced variabele niet meegeven als reference (met &). Immers, het object bestaat niet als reference, want het heeft (nog) geen reference (als in: het is nog niet toegewezen aan een variabele).

De g++ compiler zegt dan ook:

code:
1
2
3
test.cpp: In function `void f3 ()':
test.cpp:4: could not convert `S3()' to `S3 &'
test.cpp:3: in passing argument 1 of `g1 (S3 &)'


Oftewel, S3() returnt een non-referenced object en kan dus niet als reference worden meegegeven, omdat het nog geen reference heeft.
Een simpele werkende workaround is als volgt:
code:
1
2
3
struct S4 { S4 (); S4 & ref () { return *this; } private: S4 (S4 const &); };
void g2 (S4 &);
void f4 () { g2(S4().ref()); }

Mijn tweede vraag luidt als volgt: Als je met zo'n simpele workaround hetzelfde effect kan verkrijgen, wat is dan het nut van deze restrictie, en wat hebben lvalues hiermee te maken?
Je geeft nu niet het object an sich mee (tenminste: niet per definitie), maar je geeft het object wat gereturnt wordt door ref() mee. Dit is niet per definitie het object zelf (nu toevallig wel) maar compileert dus blijkbaar wel. De compiler is ook niet alleswetend. De ideale compiler zou hier dezelfde warning geven.

[edit]
Ik blaat vaag, ik zal even proberen dat eerste te verduidelijken. Als je een constructor gebruikt, dan zal het object pas in het geheugen worden geplaatst door middel van het toekennen van dat object aan een variabele:
variabele = new Object();
Dan bevat 'variabele' nu het object, en kun je 'variabele' als reference meegeven aan nieuwe functies. Immers, het heeft een plek in het geheugen.
Het meegeven van een copy kan net zomin, om dezelfde reden. Eerst moet je de copy toekennen aan een variabele, dan pas kun je het meegeven als reference, anders kun je namelijk geen geheugenplek meegeven als reference - immers, het object bestaat nog niet in het geheugen. Dat het compileert is echter een imperfectie van de compiler, niet een fix voor het probleem.

Is dit begrijpelijker? :P.

Verwijderd

Topicstarter
Dat temporaries niet gebonden zijn aan variabelen (geen naam hebben) is juist het hele idee van temporaries.

Het bestaan van een temporary object is zonder meer genoeg om er een const reference mee te kunnen initializeren (hier is geen eerdere binding aan een variabele voor nodig). Jouw bewering dat compilers zowieso fout zitten wanneer ze dit toelaten is volgens mij incorrect. Wat ik mij echter nog altijd afvraag, is waarom dit per sé een const reference moet zijn..

Ps. Bij new X(); is er overigens geen sprake van een temporary.

  • ^Mo^
  • Registratie: Januari 2001
  • Laatst online: 04-11-2025
Verwijderd schreef op 11 augustus 2002 @ 22:47:
Dat temporaries niet gebonden zijn aan variabelen (geen naam hebben) is juist het hele idee van temporaries.

Het bestaan van een temporary object is zonder meer genoeg om er een const reference mee te kunnen initializeren (hier is geen eerdere binding aan een variabele voor nodig). Jouw bewering dat compilers zowieso fout zitten wanneer ze dit toelaten is volgens mij incorrect. Wat ik mij echter nog altijd afvraag, is waarom dit per sé een const reference moet zijn..

Ps. Bij new X(); is er overigens geen sprake van een temporary.
Omdat een statement als g3() = 4 ongeldig is, en dat kan dus met een reference voorkomen.
Voorbeeltje:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    void ChangeInt( int &ioInteger)
    {
        ioInteger += 6;
    }

    int GetInt()
    {
        return 4;
    }

    int a = 4;
    ChangeInt( a); // Geldig
    ChangeInt( 4); // Ongeldig, want 4 is een constante.
    ChangeInt( GetInt()); // Ook ongeldig, wat GetInt teruggeeft kan geen waarde aan worden toegekend...

"There are 10 kinds of people in the world, those who understand binary and those who don't" | Werkbak specs


Verwijderd

Topicstarter
_Mo_ schreef op 11 augustus 2002 @ 23:15:
[...]
Omdat een statement als g3() = 4 ongeldig is, en dat kan dus met een reference voorkomen.
Zonder declaratie van g3 en verdere context kan ik hier vrij weinig mee :/.

Overigens is iets als dit:
code:
1
2
3
struct S {};
S g ();
void f () { g() = S(); }

volgens de Comeau compiler geheel legaal.
Voorbeeltje:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    void ChangeInt( int &ioInteger)
    {
        ioInteger += 6;
    }

    int GetInt()
    {
        return 4;
    }

    int a = 4;
    ChangeInt( a); // Geldig
    ChangeInt( 4); // Ongeldig, want 4 is een constante.
    ChangeInt( GetInt()); // Ook ongeldig, wat GetInt teruggeeft kan geen waarde aan worden toegekend...
De gereturnde int is volgens mijn compiler geen lvalue. Alhoewel ik ook niet begrijp waarom dit precies illegaal is, zie ik het verband niet met mijn oorspronkelijke kwestie..

  • ^Mo^
  • Registratie: Januari 2001
  • Laatst online: 04-11-2025
Verwijderd schreef op 11 augustus 2002 @ 23:28:
[...]
De gereturnde int is volgens mijn compiler geen lvalue. Alhoewel ik ook niet begrijp waarom dit precies illegaal is, zie ik het verband niet met mijn oorspronkelijke kwestie..
Omdat hier: void f3 () { g1(S3()) } precies hetzelfde staat als ChangeInt( GetInt());
De compiler denk dat jij met de reference de waarde wil veranderen, maar als je een constante meegeeft, kan je daar geen waarde aan toekennen.
Een statement als 4 = 5 levert ook de error '4' not an lvalue op (tenminste, zou ik zo denken :))

"There are 10 kinds of people in the world, those who understand binary and those who don't" | Werkbak specs


Verwijderd

Topicstarter
_Mo_ schreef op 11 augustus 2002 @ 23:35:
De compiler denk dat jij met de reference de waarde wil veranderen, maar als je een constante meegeeft, kan je daar geen waarde aan toekennen.
In de openingpost van dit topic demonstreerde ik reeds dat temporaries niet const zijn. In mijn vorige reply demonstreerde ik dat het probleem dat jij aankaart zich alleen voor built-in's lijkt voor te doen.

Het mysterie blijft vooralsnog onopgelost ;)

[ Voor 0% gewijzigd door Verwijderd op 11-08-2002 23:55 . Reden: typo ]


Verwijderd

Volgens mij gaat het erom dat een temporary geen lvalue (adres) heeft, en maar in mindere mate of hij al dan niet const is.

Het idee achter een temporary is dat hij "weggecompiled" kan worden tot een registerinhoud of een directe instructie. De compiler zal dat dan ook proberen, maar wordt gedwongen een temporary in RAM te schrijven als je impliciet of expliciet gebruik maakt van de this pointer (daarom werkt je ref() hack).

Waarom wordt er bij non-const references naar temporary objects dan niet gewoon ook die temporary naar RAM geschreven? Omdat de reference een andere scope kan hebben dan de temporary. Je kunt nooit garanderen dat de reference out-of-scope gaat voor de temporary out-of-scope gaat, omdat hij gepropageerd kan worden (in een object op de heap bv.).

[ Voor 0% gewijzigd door Verwijderd op 12-08-2002 00:13 . Reden: typo rvalue != lvalue ]


Verwijderd

Ik zie ook niet echt waarom het al dan niet const zijn zo'n mega verschil zou zijn - kun je dat alsjeblieft rgens verduidelijken? Ik mis dat kleine beetje... Je hebt het waarschijnlijk al ergens gedaan, maar het is me onduidelijk... :{.

offtopic:
(Sorry, maar ik heb dus geen ICT opleiding gedaan ;) - breekt me hier een beetje op vrees ik)


Net als mietje en _Mo_ denk ik dus dat het komt omdat een temporary geen adres heeft...

Verwijderd

Topicstarter
Zojuist ontdekte ik dat de volgende code:
code:
1
2
3
struct S { S (); private: S (const S &); };
void g (const S &);
void f () { g(S()); }

ook een 'inaccessible S::S(const S &)' error genereert. Mijn aanname dat initialisatie van const references direct (zonder kopie) met een temporary kan, was incorrect: bij het initialiseren van een reference met een temporary komt dus zowieso altijd een copy construction kijken.

Mietje's theorie dat een expliciete temporary niet weg-geöptimized mag worden als de this pointer gebruikt wordt lijkt me een stap in de goede richting, maar ik zou eerder geneigd zijn naar een regel als: 'de temporary mag niet weg-geöptimized worden als er een non-const method op wordt aangeroepen'. Ik zal nog eens kijken of ik een dergelijke regel in de standaard kan vinden.

Verwijderd

Topicstarter
Volgend probleem :) :

De volgende code is een poging mijn ref-workaround wat eleganter te maken:
code:
1
struct S { operator S & () { return *this; } };

Tot mijn grote schrik leverde dit de volgende warning op:
S::operator S &() will not be called for implicit or explicit conversions
Waarom niet :?.

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 21-08 17:14
Goed, ik geloof dat er wat verwarring is.
De eigenlijke reden is heel simpel: Temporaries binden alleen aan non-const-reference omdat de regel zo is. Die regel is gemaakt om te voorkomen dat een Set(X&) functie per ongeluk een temporary wijzigt die vervolgens weggegooid wordt.
Wat Sneechy terecht opmerkt is dat je er met een omweg onderuit kunt. Ook dat is meer opzet dan toeval: de filosofie van C++ is dat het je beschermt tegen Murphy, niet tegen Machiavelli. De code die nodig is is geeft al een hint dat er ongebruikelijke dingen gebeuren.

Waar je ook rekening mee zou moeten houden is dat temporaries kunnen onstaan doot typeconversies:

void add_5( long& l );
int i = 3;
add_5( i );

Zonder de regel zou er een tijdelijke long gemaakt moeten worden, die met 5 wordt opgehoogd. i zou 3 blijven. Dat wil je niet. Was de functie daarentegen

long add_6( long const& l );

geweest, dan was i = add_6( i ); volstrekt redelijk geweest. Dus in zo'n situatie wil je wel een cast toestaan, en die temporary moet je dus aan zo'n const& binden.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Verwijderd schreef op 11 augustus 2002 @ 23:28:
De gereturnde int is volgens mijn compiler geen lvalue.
De standaard zegt dit:

"A function call is an lvalue if and only if the result type is a reference."

Dus je compiler heeft helemaal gelijk ;)

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 21-08 17:14
Verwijderd schreef op 12 augustus 2002 @ 01:26:
Volgend probleem :) :

De volgende code is een poging mijn ref-workaround wat eleganter te maken:
code:
1
struct S { operator S & () { return *this; } };

Tot mijn grote schrik leverde dit de volgende warning op:
[ ... operator onbruikbaar... ]
Waarom niet :?.
Omdat het een user-defined conversion is. De "normale" binding van S objecten aan S references is een built-in conversion. Built-in conversies gaan voor user-defined conversies.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Verwijderd schreef op 12 augustus 2002 @ 01:26:
Volgend probleem :) :

De volgende code is een poging mijn ref-workaround wat eleganter te maken:
code:
1
struct S { operator S & () { return *this; } };

Tot mijn grote schrik leverde dit de volgende warning op:
[...]
Waarom niet :?.
Hierom niet:

"A conversion operator is never used
to convert a (possibly cvqualified)
object to the (possibly cvqualified)
same object type (or a reference to
it
), to a (possibly cvqualified)
base class of that type (or a reference to it), or to (possibly cvqualified)
void."

  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Kan je trouwens uitleggen waarom je dit precies wilt doen. Want met je "work-around" doe je eigenlijk iets heel riskants. (een non-const ref naar een temporary, dit wekt de indruk dat je het object "mag" veranderen) Ik heb sterk het vermoeden dat je iets specifieks wilt doen, dat ook op een andere, veiligere manier kan.

edit:
Bv. met een const_cast<> dan is het in ieder gevalk duidelijk dat je "iets vaags" doet ;)

Verwijderd

Topicstarter
Zoijar schreef op 12 augustus 2002 @ 12:01:
Kan je trouwens uitleggen waarom je dit precies wilt doen. Want met je "work-around" doe je eigenlijk iets heel riskants. (een non-const ref naar een temporary, dit wekt de indruk dat je het object "mag" veranderen) Ik heb sterk het vermoeden dat je iets specifieks wilt doen, dat ook op een andere, veiligere manier kan.

edit:
Bv. met een const_cast<> dan is het in ieder gevalk duidelijk dat je "iets vaags" doet ;)
Ik had een class gemaakt die bij het copy constructen het originele object moest wijzigen. Dit was nodig omdat het nieuwe object ownership over een bepaalde resource moest overnemen van het originele object. Precies hetzelfde als std::auto_ptr dus.

Ik heb het inmiddels ook opgelost op de std::auto_ptr manier:
code:
1
2
3
4
5
6
7
8
9
10
11
12
class S
{
  struct OwnershipTransferrer { S & src; OwnershipTransferrer (S * src_) : src (*src_) {} };

  public:

    S ();
    S (S &);
    S (OwnershipTransferrer ot) { /* haal het ownership bij ot.src weg */ }

    operator OwnershipTransferrer () { return OwnershipTransferrer(this); }
};

Zodat ik dit kan doen:
code:
1
2
3
S g () { return S(); } // returnen vanuit een functie
void h (S);
void f () { h(S()); } // meegeven aan een functie

Overigens lijkt deze methode niks anders te zijn dan een 'nettere' vorm van de ref-hack. Is dit dan ook riskant? (std::auto_ptr doet het ook zo..)

Verwijderd

Verwijderd schreef op 12 augustus 2002 @ 01:13:
Mietje's theorie dat een expliciete temporary niet weg-geöptimized mag worden als de this pointer gebruikt wordt lijkt me een stap in de goede richting, maar ik zou eerder geneigd zijn naar een regel als: 'de temporary mag niet weg-geöptimized worden als er een non-const method op wordt aangeroepen'. Ik zal nog eens kijken of ik een dergelijke regel in de standaard kan vinden.
Ik kan de regel niet expliciet vinden, maar dit lijkt mij toch de reden. Als je een (non-static) method van een object aanroept, dan wordt in principe de this pointer gebruikt. De compiler kan dit gebruik nog wegoptimizen als er geen datamembers veranderd worden (bij const-methods dus).
Verwijderd schreef op 12 augustus 2002 @ 14:53:
Overigens lijkt deze methode niks anders te zijn dan een 'nettere' vorm van de ref-hack. Is dit dan ook riskant? (std::auto_ptr doet het ook zo..)
Nee, dit is wel degelijk iets anders, je retourneert nu namelijk geen reference naar de originele temporary, maar een nieuwe (temporary) kopie van die originele temporary. Dit is dus ongevaarlijk.

  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Je zou ook zoiets kunnen doen: (maar de std::auto_ptr manier heeft waarschijnlijk wat voordelen bij thread safety etc. en is mooier. Geef dit alleen als voorbeeld. Dit
edit:
de auto_ptr manier
is trouwens heel iets anders dan wat ik uit jouw verhaal opmaakte. Alles is geencapsuleerd nu, dus niet riskant.)

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

class Foo {
public:
    Foo() {this->owns_ = false;}

    Foo(const Foo& src) {this->transfer(src);}
    Foo& operator=(const Foo& src) {this->transfer(src); return *this;}

    void aquire() {owns_ = true;}
    void show() const {std::cout << owns_ << std::endl;}

private:
    void transfer(const Foo& src) {
        this->owns_ = src.owns_;
        src.owns_ = false;
    }

    mutable bool owns_;
};

Foo bar(Foo f) {
    f.show();
    return f;
}

int main() {
    Foo a;
    a.aquire();
    a.show();

    Foo b(a);
    a.show();
    b.show();

    a = b;
    a.show();
    b.show();

    b = bar(a);
    a.show();
    b.show();

    return 0;
}

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 21-08 17:14
Verwijderd schreef op 12 augustus 2002 @ 01:13:
Zojuist ontdekte ik dat de volgende code:
code:
1
2
3
struct S { S (); private: S (const S &); };
void g (const S &);
void f () { g(S()); }

ook een 'inaccessible S::S(const S &)' error genereert. Mijn aanname dat initialisatie van const references direct (zonder kopie) met een temporary kan, was incorrect: bij het initialiseren van een reference met een temporary komt dus zowieso altijd een copy construction kijken.

Mietje's theorie dat een expliciete temporary niet weg-geöptimized mag worden als de this pointer gebruikt wordt lijkt me een stap in de goede richting, maar ik zou eerder geneigd zijn naar een regel als: 'de temporary mag niet weg-geöptimized worden als er een non-const method op wordt aangeroepen'.
Het feit dat je een inaccessible copy ctor krijgt betekent niet dat die copy ctor gebruikt wordt. Er is een explicite regel die zegt dat er een access check plaats vindt als de copy ctor gebruikt kan worden, zelfs als deze call naar een copy ctor weggeoptimaliseerd wordt. Als je een temporary aan een const& bindt heb je zo'n situatie waar de compiler de access moet checken. Dit voorkomt portability problemen.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein

Pagina: 1