3

So i'm trying to create a base class that has a default deallocation function for that type of base classes. Depending on how I delete a derived object I'm seeing different behaviour, maybe anyone can shed some light on why I'm not seeing my override working in the below commented cases where the custom deallocation is not invoked:

#include <iostream>

struct B {
    void operator delete(void* ptr) { 
        std::cout << "B's operator delete" << std::endl;
        ::operator delete(ptr); 
    }
};

struct D : B {
};

template<typename T>
class E {
public:
    E(T* inst) {
        //delete inst;            // invokes the operator delete override   
        T::operator delete(inst); // invokes the operator delete override
        //operator delete(inst);  // does not invoke the operator delete override
    }
};

int main() {
    D* dp = new D();
    E<D>* ep = new E<D>(dp);
    delete ep;
}

I'm guessing that the last attempt (invoking operator delete) without T:: uses the global deallocation function instead of my override, but why is that when delete inst is working just fine without having to specify T::?

I was expecting all three statements to actually invoke the delete operator for the object if it has been overriden. Can this be controlled through anything or is this correctly following the C++ ABI?

  • Are you sure that you don't just want to have a virtual destructor instead? – Aconcagua Apr 08 '19 at 09:31
  • I chose the wrong dupe, and a I can't find another suitable one, but naming the function `operator delete` is a red herring. – StoryTeller - Unslander Monica Apr 08 '19 at 09:33
  • You are never, ever supposed to call `operator delete(objectptr)`. Only `delete objectptr`. You can call `somescope::operator delete(voidptr)` inside another `operator delete` function. – n. m. could be an AI Apr 08 '19 at 09:38
  • This is just minimal example code, I would like to be able to invoke the deallocator without running the destructor for a given typename. What do you mean @StoryTeller ? – Philip Meulengracht Apr 08 '19 at 09:39
  • That [this](http://coliru.stacked-crooked.com/a/9e103221a63afe78) is even more minimal, with the same exact issue. – StoryTeller - Unslander Monica Apr 08 '19 at 09:45
  • Not the same, that behaviour to me is obvious, this behaviour is involving operators and not providing me with what I would call consistent behaviour. – Philip Meulengracht Apr 08 '19 at 09:47
  • "An operator" is nothing but a function with a special name that you may call via additional syntax. `operator delete(blarg)` and `foo(blarg)` are exactly the same sort of function call. – StoryTeller - Unslander Monica Apr 08 '19 at 09:49
  • An operator is just a function. You very deliberately wrote a statement that called a different function. Why would you _ever_ write `operator delete(inst)` _unless_ you wanted to select the global operator? In all normal use you'd just write `delete inst`, surely? – Useless Apr 08 '19 at 09:50
  • Yea thanks for trying to explain it to me both of you - I simply thought both delete and operator delete would act the same way except for destructors not being run – Philip Meulengracht Apr 08 '19 at 09:54
  • " I would like to be able to invoke the deallocator without running the destructor" Why do you want to do mean things? The destructor is there to be ran. – n. m. could be an AI Apr 08 '19 at 16:28
  • Sometimes you want to be able to allocate and deallocate space for an object that has certain allocation requirements without actually initializing or finalize the object itself – Philip Meulengracht Apr 09 '19 at 05:44

2 Answers2

1

Not totally sure about this, but I think this is a scenario Stephen Dewhurst calls the "Operator Function Lookup Anomaly" in C++ Gotchas #23. He calls the explicit form to call an operator overload the "function call syntax" and shows an example in which infix syntax and function call syntax differ with respect to name lookup.

class X {
    public:
      X &operator %( const X & ) const;
      // ...
};

X &operator %( const X &, int );

void X::f() {
    X &anX = *this;
    anX % 12; // OK, non-member
    operator %( anX, 12 ); // error!
}

The use of the function call syntax follows the standard lookup sequence in searching for the function name. In the case of the member function X::f, the compiler will first look in the class X for a function named operator %. Once it finds the name, it won’t continue looking in outer scopes for additional functions named operator %.

Let's apply this to the operator delete scenario.

template<typename T>
class E {
  public:
    E(T* inst) {
        // delete inst; // Ok, infix call syntax
        // T::operator delete(inst); // Ok, qualified lookup enforced
        operator delete(inst); // The "anomaly"! This finds the operator delete for E
    }
};

Here, the name lookup stops at the operator delete that E provides, which is the default, global implementation. You can verify this by adding an operator delete member function to E.

lubgr
  • 37,368
  • 3
  • 66
  • 117
  • Thanks for a detailed explaination! I was under the impression that invoking ```operator delete(inst)``` would be the same as ```delete inst```, just simply without performing the destructor. Is it at all possible to get the same behaviour without prefixing with T:: ? – Philip Meulengracht Apr 08 '19 at 09:51
  • Well it's possible by using the infix call syntax `delete inst;`, this is without prefixing. But if you want the verbose function call syntax - no, it doesn't seem to be possible. – lubgr Apr 08 '19 at 09:53
1

A deallocation function (operator delete) for a class is a static member function of that class [class.free]/5. A delete expression has the special property that the lookup for a deallocation function also considers class-specific deallocation functions [expr.delete]/9 [class.free]/4, which is why

delete inst;

will indeed call the correct deallocation function. When you're just manually writing

operator delete(inst);

then that really is nothing but a call to the family of functions called operator delete. Such a call will just go through normal overload resolution, which will not consider static member functions unless explicitly given a qualified-id, which is what

T::operator delete(inst);

does and why that "works" as well…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • Thanks a lot for the explaining, I thought that delete and operator delete both would act the same way except for one performing destructors and the other not – Philip Meulengracht Apr 08 '19 at 09:52
  • @PhilipM it's a common misconception that using an operator in an expression is the same thing as calling an operator function. An operator function is just a function with a special name that the compiler will invoke during evaluation of certain kinds of expressions to implement certain parts of the behavior of that kind of expression. The `new` and `delete` expressions are perfect examples for this. A *new-expression* will do much more than just call `operator new`. It creates an actual object, which may involve calling a constructor, handles potential failure of the allocation function, … – Michael Kenzel Apr 08 '19 at 10:03