9

I recently ran into this problem at work. A library I'm using makes use of reference counted objects and implements its own way of handling it. Part of the implementation is that each class of the library has a private destructor. Im guessing this is to prevent creation of objects on the stack as the library manages objects lifetime automatically (Its a scene graph).

Anyways, I wanted to allocate an array of such a class on the heap and ran into the following problem:

#include <iostream>

using namespace std;

    class test
    {
    public:
        test() {
            cout << "ctor" << endl;
        }

        //~test() = delete; also doesnt work

    private:
        ~test()
        {
            cout << "dtor" << endl;
        }

    };

    int main()
    {
        //works
        auto oneInstance = new test;

        //doesnt work
        auto manyInstances = new test[3];
    }

The array allocation produces the following error using GCC:

source_file.cpp: In function ‘int main()’:
source_file.cpp:15:5: error: ‘test::~test()’ is private
     ~test()
     ^
source_file.cpp:26:36: error: within this context
     auto manyInstances = new test[3];
                                    ^

Why does the destructor need to be public/available in order to allocate an array of this class on the heap? It works fine when only allocating a single instance like in the line before. I also tried using the more modern "delete" syntax, but it produced the same result.

Is there any kind of magic in the new[] operator that I'm not aware of?

EDIT:

Thanks for the quick help. Im wondering why this code doesnt print "dtor" twice though:

#include <iostream>

using namespace std;

class test
{
public:
    test() {
        static int allocations = 0;
        ++allocations;

        if(allocations == 3)
        {
            //produce exception
            throw 1;
        }
        cout << "ctor" << endl;
    }

    ~test()
    {
        cout << "dtor" << endl;
    }

};

int main()
{
    //works
    auto oneInstance = new test;

    //doesnt work
    try {
    auto manyInstances = new test[3];
    }
    catch(...)
    {
            cout << "error?";
    }
}

This prints:

ctor ctor dtor error?
trincot
  • 317,000
  • 35
  • 244
  • 286
Mille25
  • 101
  • 5
  • 1
    This would also fail with `auto manyInstances = test()` and work with `auto manyInstances = new (test*)[3];`. – Jay Jun 27 '17 at 06:36
  • Regarding your edit, you only successfully construct 2 instances of `test`. The first is pointed to by `oneInstance` and never deleted, so its destructor is never called. The second is destroyed by `new[]` after constructing the second element of the array throws. – Miles Budnek Jun 27 '17 at 06:58
  • But the construction of the second element doesnt fail, does it? Just the third fails. The ctor print is after throw. – Mille25 Jun 27 '17 at 07:05
  • Yes, the third throws. So if `oneInstance` were ommitted then two would be destroyed. No destructor call is made for an object who's constructor throws. – Miles Budnek Jun 27 '17 at 07:06
  • Im dumb, you are correct. Thanks! For some reason I completely forgot about the oneInstance allocation. – Mille25 Jun 27 '17 at 07:10
  • @BaummitAugen not really a duplicate IMO, there is also the question of what `throw;` does in OP's question and in my answer. Not to mention OP asking for an explanation of his output. Which in my opinion is a valid question, especially given that he has good sample code – Curious Jun 27 '17 at 18:38
  • @Curious The `throw;` is not even in the question atm, and the second part is a dupe anyways. – Baum mit Augen Jun 27 '17 at 20:13
  • @BaummitAugen the question was edited to not be asking that. Still, the answer has that so should be useful to people. I can edit the title of the question to include it. Also even if the `throw 1` and the number of constructions was the question that should be valid? I feel like if a question combines two already answered questions into one example where someone gets confused because of how both things interact, that still remains a valid question, don't you think? – Curious Jun 28 '17 at 01:10
  • @Curious As I said, the other part is just another duplicate, and not even a hard to find one. Of course the question is valid, it's just been asked before and is thus a duplicate. – Baum mit Augen Jun 28 '17 at 01:17
  • @BaummitAugen Ok. You know probably know best. But I feel like this question could be reopened. – Curious Jun 28 '17 at 01:20
  • @BaummitAugen that was also a part of the question and one of the reasons i think it should be reopened. They talked about that in the comments above – Curious Jun 28 '17 at 01:26
  • @Curious Ah right, thanks. – Baum mit Augen Jun 28 '17 at 01:26
  • @BaummitAugen think it should be reopened then :) ? – Curious Jun 28 '17 at 01:27
  • 1
    @Curious Nah. *"Im dumb, you are correct. Thanks! For some reason I completely forgot about the oneInstance allocation."* is basically a typo. :) (Fyi, score in reopen queue is 1 leave closed, 1 reopen, so let's see how the community decides.) – Baum mit Augen Jun 28 '17 at 01:29

1 Answers1

13

It's because of exceptions, the array version of new[] has to go and call the destructor on elements that have been previously allocated when an exception propagates to ensure exception safety. The single element new does not need to do that. If allocation fails, it just fails, no need to destroy anything.

§ 8.3.4 New [expr.new/20]

If the new-expression creates an array of objects of class type, the destructor is potentially invoked


Regarding your edit, see the following quote from the C++17 standard

§ 8.17 Throwing an exception [expr.throw/4]

If no exception is presently being handled, evaluating a throw-expression with no operand calls std::terminate()


Regarding your second edit, you have missed counting an instance of test that you created via new (not new[]), that leads to the first instance of test being created, that's where the confusion about number of constructions comes from.

Curious
  • 20,870
  • 8
  • 61
  • 146