C++ RAII not working?
Asked Answered
C

10

13

I'm just getting started with RAII in C++ and set up a little test case. Either my code is deeply confused, or RAII is not working! (I guess it is the former).

If I run:

#include <exception>
#include <iostream>
class A {
public:
    A(int i) { i_ = i; std::cout << "A " << i_ << " constructed" << std::endl; }
    ~A() { std::cout << "A " << i_ << " destructed" << std::endl; }
private:
    int i_;
};

int main(void) {
    A a1(1);
    A a2(2);
    throw std::exception();
    return 0;
}

with the exception commented out I get:

A 1 constructed
A 2 constructed
A 2 destructed
A 1 destructed

as expected, but with the exception I get:

A 1 constructed
A 2 constructed
terminate called after throwing an instance of 'std::exception'
  what():  std::exception
Aborted

so my objects aren't destructed even though they are going out of scope. Is this not the whole basis for RAII.

Pointers and corrections much appreciated!

Cavendish answered 9/7, 2009 at 14:8 Comment(3)
You too found a bug in C++! =)Research
If an exception escapes main() it is implementation defined weather the stack is unwound. Thus add a try{}catch(...){} block to main and you will be fine.Ballinger
Don't you mean RIAA?Brakpan
C
20

You don't have a handler for your exception. When this happens the standard says that std::terminate is called, which in turn calls abort. See section 14.7 in The C++ Programming Language, 3rd edition.

Cresting answered 9/7, 2009 at 14:16 Comment(3)
You actually can set your own terminate function, but I don't know that it's a really useful capability.Randeerandel
Well, it could be used, say, to log the termination to a specific file or send an email to somebody.Zugzwang
As RaphaelPS pointed out, Writing your own terminate function is useful if you want to log the termination or cleanup global resources. You cannot, however, recover from terminate. A terminate handler takes no arguments, has no return value, and is expected to exit the application.Cresting
L
18

The problem is that main has a special status. When an exception is thrown from there, the stack can't be meaningfully unwound, the application just calls std:terminate instead.

And then it makes a bit of sense why the variables don't go out of scope. We haven't actually left the scope in which they were declared. What happens could be considered to be equivalent to this:

int main(void) {
  A a1(1);
  A a2(2);
  std::terminate();
}

(I believe it is implementation-defined whether destructors are called in this case though, so on some platforms, it'll work as you expected)

Lodger answered 9/7, 2009 at 14:18 Comment(8)
Why is that implementation defined? Wouldn't it make more sense if constructors are always called? C++ can just be silly sometimes...Fig
Not silly - practical. Forcing destructors (not constructors) to be always called would limit the ways that EH and stack unrolling can be implemented.Poohpooh
It may be silly, but it is also pragmatic. The C++ spec tries to avoid limiting implementers. Ideally, they want it to be possible to create a standards-conforming implementation of C++ on any CPU and any OS. And since the main function in a sense marks the boundary between OS and C++, they don't want to assume too much about it. You're right, in the real world, it would be nice if they didn't leave this particular detail up to the implementation. We know that now, but was it as obvious in 1998, when the language was being standardized? They didn't want to paint themselves into a cornerLodger
@jalf: maybe it's time to revisit the issue?Amicable
@just: For what gain? There might still be implementations somewhere that are only possible with this extra bit of freedom. Moreover, it'd cause confusion (does my compiler implement the new, strict, behavior, or hasn't it been updated yet?) and it's just not a big deal, is it? It may be surprising the first time you encounter this issue, but it's fairly easy to discover and fix when it becomes an issue in your code.Lodger
@jalf: The gain is obvious, but you're asking about net gain, which is less so. I don't have a strong feeling about this either way, but I feel your arguments are a bit weak. 1) would-be non-conformat implementations would have enough time and space to make the comittee aware of their situation (and I'm sure Stroustrup wouldn't be happy to leave them in the dust since support for constrained systems is one of his bragging points), 2) confusing semantic changes in C++ have precedents, auto is an obvious example. but you're more likely to get compile-time, not runtime, errors with auto.Amicable
My point was actually not that non-conformant implementations would object, but that there would necessarily be a transition period where some implementations had fixed this behavior, and others hadn't yet, causing confusion, if only temporarily. I don't really see your point about auto, since it doesn't really change the semantics of existing code.Lodger
Ultimately, the committee doesn't really like to "revisit issues" unless there are serious problems with the current specification. Partly because it may adversely affect existing code, and partly because they have plenty of other issues that are more important to deal with.Lodger
S
6

You have an unhanded exception in the main, which means a call to terminate. Try this:

int main(void)
{
    try
    {
        A a1(1);
        A a2(2);
        throw std::exception();
        return 0;
    }
    catch(const std::exception & e)
    {
        return 1;
    }


}
Suppositious answered 9/7, 2009 at 14:13 Comment(6)
So I must always wrap main in try/catch when using RAII?Cavendish
I think main() is different. Try putting everything in a function you call from main().Aleida
No, only when you do something that can throw an exception.Soilure
...or at least, make sure you put a try/catch in the outermost block in main().Aleida
Not "always", only in main(). Try moving the code to a separate function and leave the exception handler in main.Zoara
You're going to have to try{}catch() somewhere, otherwise you'll just terminate when you get to the outermost scope.Strudel
B
6

If an exception escapes main() it is implementation defined weather the stack is unwound.

try

int main()
{
    try
    {
        doWork(); // Do you experiment here. 
    }
    catch(...)
    {   /*
         * By catching here you force the stack to unwind correctly.
         */
        throw;  // re-throw so exceptions pass to the OS for debugging.
    }
}
Ballinger answered 9/7, 2009 at 16:53 Comment(0)
R
5

As others have pointed out, you've got an uncaught exception, which calls terminate(). It is implementation-defined (see the Standard, 15.3 paragraph 9 and 15.5.1 paragraph 2) whether destructors are called in this case, and the definition in your implementation is apparently "No, they won't". (If terminate() is called for any other reason than throwing an exception that doesn't have a handler, destructors will not be called.)

Randeerandel answered 9/7, 2009 at 14:23 Comment(0)
V
4

Your A objects are not being destroyed because std::terminate is being called.

std::terminate is called when an unhandled exception leaks out of main. If you wrap your code in a try/catch (even if the catch just re-raises) you'll see the behaviour you were expecting.

Vernice answered 9/7, 2009 at 14:26 Comment(0)
S
3

You are not handling the exception properly, so your application is exiting before the objects go out of scope.

I am going to explain a little more. If an exception "bubbles" up to main the stack is unwound (edit). Even moving the code to a secondary function will not fix this issue. ex:

      1 #include <exception>
      2 #include <iostream>
      3
      4 void test();
      5
      6 class A {
      7     public:
      8         A(int i) { i_ = i; std::cout << "A " << i_ << " constructed" << std::endl; }
      9         ~A() { std::cout << "A " << i_ << " destructed" << std::endl; }
     10     private:    int i_;
     11 };
     12
     13
     14 int main(void) {
     15     test();
     16     return 0;
     17 }
     18
     19 void test(){
     20             A a1(1);
     21             A a2(2);
     22            throw std::exception();
     23 }

The above code will not solve the issue. The only way to solve this is to wrap the thrown exception in a try-catch block. This will keep the exception from reaching the main, and stop termination that is happening before the objects go out of scope.

Soilure answered 9/7, 2009 at 14:15 Comment(3)
Even if you move the code to another function if you don't have a try/catch it will bubble to the main and cause the same problem. So "main" isn't special at all.Soilure
You're wrong about the "undefined behavior". The behavior of an exception without a handler is defined in the Standard, except that whether or not the stack is unwound is implementation defined.Randeerandel
Changed answer to reflect this.Soilure
C
1

Others have suggested putting a try/catch inside main() to handle this, which works fine. For some reason I find the rarely used 'function-try-block' to look better, which surprises me (I thought it would look too weird). But I don't think there's any real advantage:

int main(void) 
try
{
    A a1(1);
    A a2(2);
    throw std::exception();
    return 0;
}
catch (...) 
{
    throw;
}

A couple of disadvantages are that since it's rarely used a lot of developers get thrown for a loop when they see it, and VC6 chokes on it if that's a consideration.

Cauca answered 10/7, 2009 at 2:39 Comment(0)
S
0

Since the exception is not handled by the time it reaches main(), it results in a call to std::terminate(), you essentially have the equivalent of

int main(void) {
  A a1(1);
  A a2(2);
  exit(1);
}

Destructors are NOT guaranteed to be called in cases where the program terminates before they go out of scope. For another hole in RAII, consider:

int main(void) {
  A *a1 = new A(1);
}
Shieh answered 9/7, 2009 at 17:4 Comment(1)
The second example is not a hole in RAII; it's an example of not using RAII.Cauca
N
0

The following code works.

#include <exception> 
#include <iostream> 

class A { 
public: 
    A(int i) { i_ = i; std::cout << "A " << i_ << " constructed" << std::endl; } 
    ~A() { std::cout << "A " << i_ << " destructed" << std::endl; } 
private: 
    int i_; 
}; 

void test() {
    A a1(1); 
    A a2(2); 
    throw std::exception(); 
} 

int main(void) { 
 try {
  test();
 } catch(...) {
 }
    return 0; 
} 
Niacin answered 5/2, 2010 at 9:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.