C++ – Can a Friend Function Have a Default Argument with a Private Destructor?

c++default-valuefriendlanguage-lawyer

In the next example the class U with private destructor has a friend function foo. And this friend function has argument of type U with default value U{}:

class U{ ~U(); friend void foo(U); };
void foo(U = {});

Clang and MSVC accept this code, but GCC rejects it with the error

error: 'U::~U()' is private within this context
    2 | void foo(U = {});
      |                ^

Demo: https://gcc.godbolt.org/z/eGxYGdzj3

Which compiler is right here, and does friendship extend on default arguments in C++?

Best Answer

C++20 [class.access]/8 provides as follows:

The names in a default argument (9.3.3.6) are bound at the point of declaration, and access is checked at that point rather than at any points of use of the default argument. Access checking for default arguments in function templates and in member functions of class templates is performed as described in 13.9.1.

However, [expr.call]/8 says:

... The initialization and destruction of each parameter occurs within the context of the calling function. [Example: The access of the constructor, conversion functions or destructor is checked at the point of call in the calling function. ...

While the "Example" text is not normative, I believe it reflects the intent; therefore, in order to read these two provisions harmoniously, we should understand that the destructor of the type of the default argument is (in my opinion, at least) not a name "in a default argument". Instead, we should view the call to the friend function as occurring in the following stages:

  1. The default argument initializer is evaluated. Due to [class.access]/8, access control during this step is done from the context of the declaration.
  2. The parameter is copy-initialized from the result of step 1. Due to [expr.call]/8, access control during this step is done from the context of the calling function.
  3. The function body is evaluated.
  4. The parameter is destroyed. Again, access control is done from the context of the calling function (irrelevant note: when exactly the destruction happens is not completely specified).

GCC shouldn't be rejecting the declaration void foo(U = {}) as there is no actual use of the destructor yet; and indeed, it is possible that foo might be called only from contexts that have access to U::~U. But if foo is called from a context that doesn't have access to U::~U, the program should be ill-formed. In such cases, I think that Clang and MSVC are wrong, because they still accept the code.

However, there is also the issue of [dcl.fct.default]/5 which states:

The default argument has the same semantic constraints as the initializer in a declaration of a variable of the parameter type, using the copy-initialization semantics (9.4). The names in the default argument are bound, and the semantic constraints are checked, at the point where the default argument appears. ...

The standard never defines what it means by "semantic constraints"; if it's assumed to include access control for both the initialization and destruction, then that might explain why Clang and MSVC seem to allow calls to foo from contexts that ought not to have access to U::~U.

But thinking about this more, I feel that this doesn't make too much sense, because it would imply that default arguments are "special" in a way that I don't think was intended. To wit, consider:

class U {
  public:
    U() = default;
    U(const U&) = default;
  private:
    ~U() = default;
    friend void foo(U);
};
void foo(U = {}) {}

int main() {
    auto p = new U();
    foo(*p);  // line 1
    foo();    // line 2
}

Here, MSVC accepts both lines 1 and 2; it seems clearly wrong to accept line 1, considering how [expr.call]/8 requires the destructor to be accessible from main. But Clang accepts line 2 and rejects line 1, which also seems absurd to me: I don't feel that the intent of the standard was that choosing to use the default argument (as opposed to providing the argument yourself) would exempt the caller from having to have access to the destructor of the parameter type.

If [dcl.fct.default]/5 appears to require Clang's behaviour, then I believe that it should be considered defective.

Related Question