4

Motivated by this not very well asked duplicate, I believe the problem deserves a new standalone clearly titled question. The following code triggers a compilation error with GCC 8.1.0 and Clang 6.0.0, but not with MSVC 19.00:

class X {
   public:
      X() /* noexcept */ { }    
   private:
      static void operator delete(void*) { }
};

int main() { 
    X* x = new X{}; 
}

From expr.new:

If any part of the object initialization described above terminates by throwing an exception and a suitable deallocation function can be found, the deallocation function is called to free the memory in which the object was being constructed, after which the exception continues to propagate in the context of the new-expression. If no unambiguous matching deallocation function can be found, propagating the exception does not cause the object's memory to be freed. [ Note: This is appropriate when the called allocation function does not allocate memory; otherwise, it is likely to result in a memory leak. — end note ]

In fact, this does not imply that the compilation error should be triggered if the matching deallocation function ::operator delete cannot be found. Or, does making it private just results in something like can be found but cannot be accessed? Which compilers are right?

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • 2
    [Cannot reproduce](http://rextester.com/QBL46016). `error C2248: 'X::operator delete': cannot access private member declared in class 'X'` – rustyx Jun 28 '18 at 13:35
  • @rustyx Uncomment `noexcept`. (For me, it originally compiled even without it, weird.) Anyway, the Standard does not say anything about `noexcept` specifier in [expr.new]. – Daniel Langr Jun 28 '18 at 13:37
  • The latest MSVC 2017 (19.14 / 15.7.4) accepts the version *with* `noexcept`. But interestingly the Intellisense (based on the EDG front end) still puts a red squiggle under the new and says that it shouldn't compile. – Bo Persson Jun 28 '18 at 13:42
  • @BoPersson Not that interesting; intellisense uses a completely different compiler & build chain than actually building. – Yakk - Adam Nevraumont Jun 28 '18 at 13:47
  • @rustyx Still I don't understand why the compilers throw errors without `noexcept`? That paragraph indicates otherwise in my opinion, doesn't it? – Daniel Langr Jun 28 '18 at 13:49
  • @Yakk - The interesting part is that, historically, the EDG compiler has been correct a lot more times than VC++ itself. If we have found a case where VC++ is correct, but EDG, g++ and clang are all wrong, that would be an extremely rare occasion. (So probably not). – Bo Persson Jun 28 '18 at 13:59

2 Answers2

4

There are two questions here:

  1. How is operator delete found even though it's private?

C++ first tries to find a name anywhere; checking access protection is a later phase.
Thus, your operator delete is found, but inaccessible.

  1. Why must my operator delete be accessible when the constructor is noexcept?

The wording "If any part of the object initialization [...] terminates by throwing an exception" suggests that the rest of the paragraph doesn't apply because of the noexcept.

However, as suggested by "any part of...", there may be exceptions inbetween the allocation and entering the constructor (while evaluating initialisers), or after exiting the constructor (while destroying initialisers).

Consider

struct Y
{
    Y() {}
    Y(const Y&) { throw "sorry"; }
};

class X {
   public:
      X(Y y) noexcept { }    
   private:
      static void operator delete(void*) { }
};

int main() { 
    Y y;
    X* x = new X{y}; 
}

where the Y copy constructor throws before you enter X's constructor, but after allocation, so the memory needs to be released.

So I think Visual C++ is wrong (again).

molbdnilo
  • 64,751
  • 3
  • 43
  • 82
3

Visual studio is being weird with the noexcept specifier. On paper, it shouldn't build. The reason is that the decllocation function is looked up independently from the allocation function.

[expr.new] / 20, 21 and 22

If the new-expression creates an object or an array of objects of class type, access and ambiguity control are done for the allocation function, the deallocation function, and the constructor. If the new-expression creates an array of objects of class type, the destructor is potentially invoked.

If any part of the object initialization described above terminates by throwing an exception and a suitable deallocation function can be found, the deallocation function is called to free the memory in which the object was being constructed, after which the exception continues to propagate in the context of the new-expression. If no unambiguous matching deallocation function can be found, propagating the exception does not cause the object's memory to be freed.

If the new-expression begins with a unary ​::​ operator, the deallocation function's name is looked up in the global scope. Otherwise, if the allocated type is a class type T or an array thereof, the deallocation function's name is looked up in the scope of T. If this lookup fails to find the name, or if the allocated type is not a class type or array thereof, the deallocation function's name is looked up in the global scope.

According to p20, the deallocation function has to be looked up since we are creating a class object. Then the deallcoation function is found successfully, and is unambiguous (it's the member). Since access specifiers are checked only after name lookup, this should cause an error. GCC and Clang are correct.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458