Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a friend function in C++ have a default argument whose type has a private destructor?

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++?

like image 554
Fedor Avatar asked Aug 09 '21 19:08

Fedor


People also ask

Can C functions have default arguments?

C has no default parameters.

Can functions have default arguments?

Default arguments are only allowed in the parameter lists of function declarations and lambda-expressions, (since C++11) and are not allowed in the declarations of pointers to functions, references to functions, or in typedef declarations.

Which of the operators can have default arguments when they are overloaded?

Of the operators, only the function call operator and the operator new can have default arguments when they are overloaded.

Can we give all arguments as default arguments in function yes or no?

You can pass data to these functions via function arguments. In addition to passing arguments to functions via a function call, you can also set default argument values in Python functions. These default values are assigned to function arguments if you do not explicitly pass a parameter value to the given argument.


Video Answer


1 Answers

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.

like image 85
Brian Bi Avatar answered Oct 20 '22 19:10

Brian Bi