5

If you have a class without a destructor:

struct A {
    ~A() = delete;
};

The standard does not let me "locally" allocate an instance of that class:

int main()
{
    A a; //error
}

But it seems like it is ok if I allocate that on free-store:

int main()
{
    a *p = new A();
}

As long as I dont call delete on that pointer:

int main()
{
    a *p = new A();
    delete p; //error
}

So my question is, why does the standard let me have a class without a destructor if I allocate it on free-store? I would guess there are some use cases for that? But what exactly?

Mac
  • 3,397
  • 3
  • 33
  • 58

3 Answers3

7

So my question is, why does the standard let me have a class without a destructor if I allocate it on free-store?

Because that's not how standard features work.

The = delete syntax you're talking about was invented to solve a number of problems. One of them was very specific: making types that were move-only or immobile, for which the compiler would issue a compile-time error if you attempted to call a copy (or move) constructor/assignment operator.

But the syntax has other purposes when applied generally. By =deleteing a function, you can prevent people from calling specific overloads of a function, mainly to stop certain kinds of problematic implicit conversions. If you don't call a function with a specific type, you get a compile-time error for calling a deleted overload. Therefore, =delete is allowed to be applied to any function.

And the destructor of a class qualifies as "any function".

The designed intent of the feature was not to make types which would be non-destructible. That's simply an outgrowth of permitting =delete on any function. It's not design or intent; it simply is.

While there isn't much use to applying =delete to a destructor, there also isn't much use in having the specification to explicitly forbid its use on a destructor. And there certainly isn't much use in making =delete behave differently when applied to a destructor.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
5

With this:

A a;

You will call the destructor upon exiting the scope (and you have deleted the destructor, hence the error). With this:

A *a = new A();

You simply don't call the destructor (because you never use delete). The memory is cleaned up at the programs completion, but you are essentially guaranteeing a memory leak.

There is no reason for c++ to disallow this behavior because this would create a very specific case to program into a compiler. For example, c++ doesn't disallow this:

int *p; *p = 5;

Even though this is obviously bad, and will always be bad. It is up to the programmer to ensure they don't do this.

There is no reason that you should delete your destructor because it is not a useful behavior.

Fantastic Mr Fox
  • 32,495
  • 27
  • 95
  • 175
  • Do you mean it is just a cute side effect of a set of language rules that allows other useful stuffs, and our language designers decide not to close it up after considering some trade-offs? –  Apr 29 '16 at 00:08
  • @NickyC Either that or it wasn't worth closing off because it would add unnecessary complexity for people writing compilers when no-one in their right mind would write this in their code on purpose. – Fantastic Mr Fox Apr 29 '16 at 00:12
  • This does really answer the question. I understand the logic behind it. But then another question is why force the free-store allocation? – Mac Apr 29 '16 at 00:13
  • 1
    @Mac I am confused about this comment, can you please elaborate, what do you mean by force the free-store allocation? – Fantastic Mr Fox Apr 29 '16 at 00:14
  • Sorry i am on my phone on the go. The only difference between local allocation or free-store is that I am responsible about calling delete. If I delete the destructor then why forbid the local allocation? I guess I am thinking that another design would be that if the destructor is deleted then it just should simply not be called when the variable goes out of scope. – Mac Apr 29 '16 at 00:23
  • @Mac: That's not the purpose of the `= delete` syntax. The purpose of that syntax is to say that an attempt to call the deleted function will fail at compile time. The fact that applying this syntax to the destructor will have the effects you've noted is not the purpose of the syntax. It's merely an outgrowth of it. – Nicol Bolas Apr 29 '16 at 00:53
  • 1
    I can think of a situation where this behavior might be useful or wanted. Though I hesitate to say it's a good idea. You can delete the destructor of a singleton class that is allocated on program start and should never be invalidated as long as the program is running. If you accidentally trigger the destructor on such a class it may be desirable to emit a compiler warning. However, this is nasty, and there are much better ways to solve the problem. – OmnipotentEntity Apr 29 '16 at 01:08
1

In a multi-threaded environment, you may be sharing the non-destructed class between threads.
If the thread that allocates memory terminates, there is no reason a pointer to that allocated memory couldn't still be in use by another thread.

The standard implies that a constructed object with dynamic storage duration does not qualify for destructor invocation.

12.4 Destructors [class.dtor]

A destructor is invoked implicitly
— for a constructed object with static storage duration at program termination,
— for a constructed object with thread storage duration at thread exit,
— for a constructed object with automatic storage duration when the block in which an object is created exits,
— for a constructed temporary object when its lifetime ends.


We can see the benefits of this through a basic memory sharing example between threads:

#include <thread>
#include <iostream>

//shared object
struct A {

    void say_hello(){ std::cout << ++value << '\n'; }
    ~A() = delete;
    int value;
};

//two threads
void creator();
void user(A* a);

//the creator allocates memory,
//gives it to another thread (via pointer),
//and then ends gracefully.  
//It does not attempt to implicitly call the destructor.
//Nor would we want it to for allocated memory we are sharing.
void creator(){

  A* a = new A();
  a->value = 0;
  std::thread t(user, a);
  t.detach();
}

//The user recieves a pointer to memory,
//and is free to continue using it
//despite the creator thread ending
void user(A* a){
  while(1){
    a->say_hello();
  }
}

//main->creator->user
int main(){
  std::thread t(creator);
  while(1){}
}

Trevor Hickey
  • 36,288
  • 32
  • 162
  • 271