Toon posts:

[c++] Destructors. Out of scope vs. delete.

Pagina: 1
Acties:

Verwijderd

Topicstarter
Probleem conext:
Zoals wel vaker ben ik weer behoorlijk aan het prutsen. Niet omdat het heel nuttig is, maar meer voor de lol en puur experimenteel ben bezig met een eigen exception hierarchy. Dus exceptions zonder gebruik te maken van de bestaande std::exception.

Misschien dat ik een heel simpel generiek probleem beschrijf in een veel te ingewikkeld of oninteressante context, het zij zo.

Exception oorzaak:
Indien er iets gebeurt in een programma wat niet de bedoeling is, kan een functie of methode er voor kiezen een exception te gooien om de aanroepende code hiervan op de hoogte brengen. Wat vaak gebeurt is dat de aanroepende code de exception afvangt en zelf ook weer een exception naar boven gooit. Meestal een wat oppervlakkigere exception. Dit kan handig zijn om ervoor te zorgen dat dat de aanroepende code niet wakker geschud wordt met nogal diepzinnige interne exception waar deze helemaal geen verstand van heeft.

Voorbeeldje in de context van een webserver:
C++:
1
2
3
4
5
6
7
8
9
10
void HTTP_server::start()
{
    TCP_server s;
    try {
        s.bind();
    } catch (TCP_bind_exception e) {
        throw HTTP_server_unable_to_start_exception();
    }
    // serve some webpages
}


Exception trace:
Maar soms is het toch handig om te weten waarom de webserver niet kon starten. Ik zou het handig vinden als je in een exception kon opslaan dat er een andere exception de oorzaak is van alle ellende. Je kan als het ware een exception stack opbouwen en dat mooi aan een gebruiker voorleggen.

Ik zat hier aan te denken:

My exception class + example usage:
C++:
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
// ------------------------------------------------------
// MY_EXCEPTION DEFINITION
// ------------------------------------------------------
class my_exception {
protected:
    exception *cause;  // MUST be a pointer
public:
    // default, don't care about cause
    my_exception() : cause(NULL)
    {}

    // specify a cause exception
    my_exception(my_exception c)
    {
        // use copy-constructor
        cause = new my_exception(c);
    }

    ~my_exception()
    {
        delete cause;
    }
}
// ------------------------------------------------------
// EXAMPLE USAGE
// ------------------------------------------------------
class TCP_bind_exception : my_exception {};
class HTTP_server_unable_to_start_exception : my_exception {};

void HTTP_server::start()
{
    TCP_server s;
    try {
        s.bind();
    } catch (TCP_bind_exception e) {
        throw HTTP_server_unable_to_start_exception(e); // save cause
    }
    // serve some webpages
}

try {
    HTTP_server s(80);
    s.start();
} catch (my_exception e) {
    my_exception *cur = &e;
    while (cur) {
        cout << cur->name() << endl;
        cout << cur->message() << endl;  // some exception statistics
        cur = cur->get_cause();
    }
}


Vroegtijdige elliminatie:
Maar dit gaat natuurlijk gruwelijk fout!!

Voornamelijk de constructor en de destructor van de my_exception class. Deze maken maken een nieuwe exception op de heap en laten dit een kopie zijn van de cause, welke gewoon op de stack leefde. Het probleem is hier dus een object wat op de stack leeft en een referentie heeft naar iets wat op de heap leeft.

Omdat er zoveel met deze exception wordt rond gezeuld, en te pas en te onpas kopieen op de stack worden gemaakt, blijft er (veel te vroegtijdig) niets meer over van de my_exception::cause attribute. Deze is vaak al lang in een voorgaande implicite kopie naar de verdoemenis geholpen met een call naar delete. Maar geen delete aanroepen in de destructor zal er voor zorgen dat ie NOOIT opgeruimd wordt, wat zal leiden tot een memory leak.

Eigen smerige suggesties:
De enige twee, nogal smerige, oplossingen die ik kon bedenken zijn het gooien van pointers naar exceptions ipv exceptions (maar dat wil niemand), of het gebruik maken van smart-pointers. De laatste lijkt mij ook geen elegante oplossing.

Heeft er iemand enig idee hoe ik dit probleem aan zou kunnen pakken?

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

H!GHGuY

Try and take over the world...

voor zover ik weet is het gebruikelijk dat excepties byref opgevangen worden:

C++:
1
2
3
4
5
6
7
8
try
{
//iets
}
catch (myexception& e)
{
//...
}

deze zal kopies van je exceptie vermijden

kijk eens naar wat een copyconstructor jou te bieden heeft want die is namelijk ook wat je zoekt.

ASSUME makes an ASS out of U and ME


Verwijderd

Topicstarter
Helaas, ook het vangen bij reference helpt niet in zo'n geval. Nog steeds worden er kopieen gemaakt van het ding.

In welke vorm kan een copy-constructor mij in deze van dienst zijn? In deze copy-constructor Zelf een kopie op de heap maken en de cause attribute van de gekopieerde op NULL zetten?

Verwijderd

Topicstarter
Het copy constructor truukje lijkt te werken. Maar ik moet wel een conversie van const naar non-const maken en het origineel wordt dus veranderd! Iets wat nogal tegen intuitief is.

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // exception copy-constructor
    exception(const exception& original)
    {
        exception *tmp = (exception*)&original;
        cause = tmp->pop();
    }

    virtual ~exception()
    {
        delete cause;
    }

    exception* pop()
    {
        exception *tmp = cause;
        cause = NULL;
        return tmp;
    }

// -------------------------------------------------------------------------
exception a;
exception b(a); // deze veranderd nu a !!


edit:
Tegen intuitief, maar wel duidelijk wat ik wil. :-)

[ Voor 6% gewijzigd door Verwijderd op 29-10-2005 19:03 ]


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

H!GHGuY

Try and take over the world...

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class myexception;

class myexception
{
private:
  myexception* innerException;
  char* message;
public:
  myexception(const char* message) : innerException(NULL)
  {
     // strcopy en andere meuk
  }

  myexception(myexception& exc) : innerException(NULL)
  {
     if (exc.innerexception != NULL)
        innerException = new myexception(*(exc.innerException));
     message = new char[strlen(exc.message)+1];
     // strcpy
  }
}


het komt erop neer dat elke copy zijn eigen hierarchie moet hebben (die net na de copy dus identiek is)
bovendien denk ik dat je redelijk wat kopies vermijd door die catch byref te gebruiken.

ASSUME makes an ASS out of U and ME


Verwijderd

Topicstarter
Het probleem is dat je 'gebruikers' niet kan dwingen een catch bij ref te gebruiken. Het is geen slecht idee, maar het moet niet de enige goede manier zijn natuurlijk.

Verwijderd

Topicstarter
Ik heb nu deze methode om te zorgen dat er geen apparte kopie van de hierarchy gemaakt hoeft te worden, maar dat het deleten toch maar een enkele keer gebeurt.

C++:
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
class exception
private
    bool delete_cause;
    exception* cause;
public:
    exception(string msg) :
        cause(NULL)
        delete_cause(false)
    { ... }

    // exception copy-constructor
    exception(const exception& original) :
        cause(NULL)
        delete_cause(false)
    {
        exception *tmp = (exception*)&original;
        cause = tmp->pop();
        delete_cause = true;
    }

    virtual ~exception()
    {
        if (delete_cause)
            delete cause;
    }

    exception* pop()
    {
        delete_cause = false;
        return cause;
    }

// -------------------------------------------------------------------------
exception a;
exception b(a); // a lijkt nu nog hetzelfde, maar zal nooit zijn cause attribute 'killen'.

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

H!GHGuY

Try and take over the world...

dat catch by reference verminderd enkel het aantal copy-constructors... en zal geen verschil maken...

wat gebeurt er trouwens bij jou als:
C++:
1
2
3
4
5
exception giveException()
{
  exception ret;
  return ret;
}


een custom copy-constructor is er nou net om een deep-copy te maken ipv een shallow-copy.
dus je instantieert een volledig nieuwe structuur identiek aan de oude (dus bvb char* effectief met new en strcpy ipv gewoon pointer copy)

en die byref passen is gewoon om het aantal copy-contstructors dat je moet aanroepen kleiner te maken.

ASSUME makes an ASS out of U and ME


Verwijderd

Topicstarter
HIGHGuY schreef op zaterdag 29 oktober 2005 @ 19:38:
dat catch by reference verminderd enkel het aantal copy-constructors... en zal geen verschil maken...

wat gebeurt er trouwens bij jou als:
C++:
1
2
3
4
5
exception giveException()
{
  exception ret;
  return ret;
}


een custom copy-constructor is er nou net om een deep-copy te maken ipv een shallow-copy.
dus je instantieert een volledig nieuwe structuur identiek aan de oude (dus bvb char* effectief met new en strcpy ipv gewoon pointer copy)
Aangezien er nu geen message en geen cause/inner- exception zijn gespecificeerd bij de contructor zal mijn copy destructor en destructor dit door hebben en niets doen. Die deep copy wil ik dus juist niet met mijn exceptions.
en die byref passen is gewoon om het aantal copy-contstructors dat je moet aanroepen kleiner te maken.
Dat is inderdaad waar, maar het was dus geen oplossing voor mijn oorspronkelijke probleem. Het is inderdaad wel een goed idee om zoveel mogelijk bij reference te catchen.

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
Duh. Als je geen werkende copy ctor hebt, dan maak je'm private undefined. Wil je'm public hebben, dan zorg je dat ie werkt. Wat er nou mis is met een shared pointer ontgaat me. Ik vermoed dat jouw code iets vergelijkbaars doet, maar in tegenstelling tot smart pointers is dat niet triviaal.

Overigens slice je je objecten ook nog..

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


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 01:30

.oisyn

Moderator Devschuur®

Demotivational Speaker

Verwijderd schreef op zaterdag 29 oktober 2005 @ 19:15:
Het probleem is dat je 'gebruikers' niet kan dwingen een catch bij ref te gebruiken. Het is geen slecht idee, maar het moet niet de enige goede manier zijn natuurlijk.
Natuurlijk wel, zeker als je een exception class hierarchy hebt. Stel je hebt een aantal exceptions die een virtual methode implementeren die beschrijft wat er fout is gegaan. Je kunt dan niet simpelweg een by-value exception van de base class afvangen; dan heb je niets meer aan die extra informatie omdat er een kopie gemaakt wordt van alleen de baseclass van de exception die gegooid werd.

Ik ben het er wel mee eens dat, al zou je 'm by value willen afvangen, je programma dan natuurlijk wel in een well-defined state moet achterlaten, óf ervoor zorgen dat het überhaupt al niet compilet (door de copy-ctor bijv. private te maken zoals MSalters al zei)

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Verwijderd

Topicstarter
MSalters schreef op zondag 30 oktober 2005 @ 00:31:
Duh. Als je geen werkende copy ctor hebt, dan maak je'm private undefined. Wil je'm public hebben, dan zorg je dat ie werkt.
Tja, mijn public copy-ctor wekt ook, maar hij maakt geen ware deep-copy. Dat hoeft toch geen probleem te zijn? Zoiets heb je toch zelf in de hand.
Wat er nou mis is met een shared pointer ontgaat me. Ik vermoed dat jouw code iets vergelijkbaars doet, maar in tegenstelling tot smart pointers is dat niet triviaal.
Als je twee non-pointer objecten hebt met een shared pointer welke in de dtor ge-delete wordt, zal er dus inconsistentie optreden indien de eerste van de twee out-of-scope geraakt.
Overigens slice je je objecten ook nog..
Zou je kunnen uitleggen wat je hier mee bedoeld?
.oisyn schreef op zondag 30 oktober 2005 @ 02:41:
[...]
Natuurlijk wel, zeker als je een exception class hierarchy hebt. Stel je hebt een aantal exceptions die een virtual methode implementeren die beschrijft wat er fout is gegaan. Je kunt dan niet simpelweg een by-value exception van de base class afvangen; dan heb je niets meer aan die extra informatie omdat er een kopie gemaakt wordt van alleen de baseclass van de exception die gegooid werd.

Ik ben het er wel mee eens dat, al zou je 'm by value willen afvangen, je programma dan natuurlijk wel in een well-defined state moet achterlaten, .....
Dat is inderdaad wel wat ik bedoelde. Natuurlijk kan je 'dwingen' dat mensen bij reference catchen, als dat nu eenmaal de beste of zelfs enige juiste methode is. Maar om je programma met memory-leaks of segmentation-faults achter te laten als dit niet gebeurt vind ik tegen intuitief en net iets te ver gaan.
..... óf ervoor zorgen dat het überhaupt al niet compilet (door de copy-ctor bijv. private te maken zoals MSalters al zei)
Indien je deze copy-ctor private maakt zal de volgende code nooit meer werken:
C++:
1
throw exception("msg");


Ieder in ieder geval bedankt voor de reacties, vage zaken worden helderder.

[ Voor 10% gewijzigd door Verwijderd op 30-10-2005 03:00 ]


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
Verwijderd schreef op zondag 30 oktober 2005 @ 02:50:
[...]
Tja, mijn public copy-ctor wekt ook, maar hij maakt geen ware deep-copy. Dat hoeft toch geen probleem te zijn? Zoiets heb je toch zelf in de hand.
Hij compilet, da's iets anders dan werken. Een shallow copy is OK, maar je originele post had een dtor die daar geen rekening mee hield. Daaarvoor moet je dus een copy-count bij houden, en kom je op een intrusive smart pointer design uit. Dan kun je maar beter direct boost::shared_ptr gebruiken, dan zie ik aan de naam meteen wat je doet.
Als je twee non-pointer objecten hebt met een shared pointer welke in de dtor ge-delete wordt, zal er dus inconsistentie optreden indien de eerste van de twee out-of-scope geraakt.
Zonder verdere voorzorgsmaatregelen: Ja. En aangezien er altijd een van de twee objecten eerst out-of-scope gaat is het dus altijd fout.
[Overigens slice je je objecten ook nog..]

Zou je kunnen uitleggen wat je hier mee bedoeld?
In je originele code had je
C++:
1
2
3
4
5
6
    // specify a cause exception 
    my_exception(my_exception c) 
    { 
        // use copy-constructor 
        cause = new my_exception(c); 
    }
Als c een TCP_bind_exception was, dan zou je een new TCP_bind_exception moeten doen. Nu is dit geval vat vreemder; het lijkt de copy constructor maar om 'm aan te roepen moet je c eerst kopieren, waarvoor je de copy constructor nodig hebt, waarvoor je eerst c moet kopieren, etcetera.
Indien je deze copy-ctor private maakt zal de volgende code nooit meer werken:
C++:
1
throw exception("msg");
Terecht. Als een object geen werkende copy ctor heeft, dan is het niet bruikbaar als exception.

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