Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[clang] Diagnose default arguments defined in different scopes #124844

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

Endilll
Copy link
Contributor

@Endilll Endilll commented Jan 28, 2025

This patch implements the rule described in N5001.[over.match.best]/4:

If the best viable function resolves to a function for which multiple declarations were found, and if any two of these declarations inhabit different scopes and specify a default argument that made the function viable, the program is ill-formed.

Consequently, it implements CWG1 "What if two using-declarations refer to the same function but the declarations introduce different default-arguments?" and CWG418 "Imperfect wording on error on multiple default arguments on a called function" that were fixed by adding paragraph 4 to [over.match.best].

In the case of friend functions, we no longer diagnose redefinitions of default arguments across scopes. This doesn't change status quo w.r.t. what we accept and what we reject, because such cases also violate [dcl.fct.default]/4.

Background information on default arguments

Sets of default arguments are associated with lexical scope of function declarations they appear in, with a single exception of out-of-line definitions of member function (details below):

For non-template functions, default arguments can be added in later declarations of a function that inhabit the same scope.
Declarations that inhabit different scopes have completely distinct sets of default arguments.

(Note that "inhabit" above is a word of power, which for our case uses the fallback definition in [basic.scope.scope].)
Typically this is the same scope as (semantic) target scope of the declaration, so all redeclarations share the same set of default arguments. However, there are function declarations that (semantically) belong to the different scope than the (lexical) scope they inhabit: extern "C" declarations, local function declarations, friend declarations, and out-of-line definitions of member functions.

Friend declarations and out-of-line definitions of member functions have additional provisions in the wording:

If a friend declaration D specifies a default argument expression, that declaration shall be a definition and there shall be no other declaration of the function or function template which is reachable from D or from which D is reachable.

Except for member functions of templated classes, the default arguments in a member function definition that appears outside of the class definition are added to the set of default arguments provided by the member function declaration in the class definition; the program is ill-formed if a default constructor ([class.default.ctor]), copy or move constructor ([class.copy.ctor]), or copy or move assignment operator ([class.copy.assign]) is so declared.
Default arguments for a member function of a templated class shall be specified on the initial declaration of the member function within the templated class.

Implementation approach

MergeCXXFunctionDecl diagnoses redefinitions of default arguments across function redeclarations in the same scope, in other words, it handles default arguments that are a part of the same set of default arguments. This patch complements it by handling default arguments that are a part of different sets.

[over.match.best]/4 applies when functions are called, so checking it in MergeCXXFunctionDecl would be too early. Sema::BestViableFunction also doesn't fit, because we can skip it if lookup finds a single function. By the time CheckFunctionCall is called, number of arguments in CallExpr is adjusted to include default arguments, so it's too late, too. ConvertArgumentsForCall seems to be the best fit for this check.

@Endilll Endilll added c++ clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jan 28, 2025
@Endilll Endilll self-assigned this Jan 28, 2025
Copy link
Contributor

@cor3ntin cor3ntin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, I left a few comments

Comment on lines +76 to +80
- Clang now diagnoses ambiguous default arguments declared in different scopes
when calling functions, implementing [over.match.best] p4.
(`CWG1: What if two using-declarations refer to the same function but the declarations introduce different default-arguments? <https://cplusplus.github.io/CWG/issues/1.html>`_,
`CWG418: Imperfect wording on error on multiple default arguments on a called function <https://cplusplus.github.io/CWG/issues/418.html>`_)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside should we augment our #GHXXXX logic to support #CWG123 and #LWG123? :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make some sense, but I still like how clickable those links are. #GHXXXX logic optimizes for writing release notes instead of reviewing them, which I find suboptimal.

Comment on lines +5148 to +5151
def err_ovl_ambiguous_default_arg
: Error<"function call relies on ambiguous default argument %select{|for "
"parameter '%1'}0">;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def err_ovl_ambiguous_default_arg
: Error<"function call relies on ambiguous default argument %select{|for "
"parameter '%1'}0">;
def err_ovl_ambiguous_default_arg : Error<
"ambiguous default argument %select{|for parameter '%1'}0">;

Comment on lines +493 to +506
if (PrevForDefaultArgs->getLexicalDeclContext()->getPrimaryContext() !=
ScopeDC->getPrimaryContext() &&
!New->isCXXClassMember())
// If previous declaration is lexically in a different scope,
// we don't inherit its default arguments, except for out-of-line
// declarations of member functions.
//
// extern "C" and local functions can have default arguments across
// different scopes, but diagnosing that early would reject well-formed
// code (_N5001_.[over.match.best]/4.) Instead, they are checked
// in ConvertArgumentsForCall, after the best viable function has been
// selected.
continue;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the comment immediately above the if

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we usually quote C++2c rather than a specific draft

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but i think the wording we want to quote here is actually
https://eel.is/c++draft/dcl.fct.default#4.sentence-2

There is a "non-template" scenario here too. But we say nothing about member functions in the wording here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have tests for that?

struct S {
    void f(int a = 0);
};

void S::f(int a = 2) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the comment immediately above the if

I'm keeping this consistent with the surrounding code.

we usually quote C++2c rather than a specific draft

Hmm, fine.

but i think the wording we want to quote here is actually
https://eel.is/c++draft/dcl.fct.default#4.sentence-2

No, the wording that you point at (together with the ODR) is what allows MergeCXXFunctionDecl to diagnose redeclarations of default arguments in the same scope early, during declaration matching.

The wording I'm pointing at explains why we need to defer check for default arguments defined in different scopes.

do we have tests for that?

Yes, l3 in default-arguments-different-scopes.cpp.

Comment on lines +5835 to +5845
// For each such parameter, collect all redeclarations
// that have non-inherited default argument.
llvm::SmallDenseMap<int, llvm::TinyPtrVector<ParmVarDecl *>> ParamRedecls(
LastDefaultArgIndex - FirstDefaultArgIndex + 1);
for (FunctionDecl *Redecl : FDecl->redecls()) {
for (int i = FirstDefaultArgIndex; i <= LastDefaultArgIndex; ++i) {
ParmVarDecl *Param = Redecl->getParamDecl(i);
if (Param->hasDefaultArg() && !Param->hasInheritedDefaultArg())
ParamRedecls[i].push_back(Param);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you iterate over i in the outer loop, you can forgo the map entirely and do it on one pass

llvm::SmallDenseMap<int, llvm::TinyPtrVector<ParmVarDecl *>> ParamRedecls(
LastDefaultArgIndex - FirstDefaultArgIndex + 1);
for (FunctionDecl *Redecl : FDecl->redecls()) {
for (int i = FirstDefaultArgIndex; i <= LastDefaultArgIndex; ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (int i = FirstDefaultArgIndex; i <= LastDefaultArgIndex; ++i) {
for (int I = FirstDefaultArgIndex; I <= LastDefaultArgIndex; ++I) {

Comment on lines +10963 to +10966
// [over.match.best]/4 is checked for in Sema::ConvertArgumentsForCall,
// because not every function call goes through our overload resolution
// machinery, even if the Standard says it supposed to.

Copy link
Contributor

@cor3ntin cor3ntin Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have test for something like that

namespace A {
    extern "C" void f(long, long = 0); // viable candidate, but not picked
}
namespace B {
    extern "C" void f(long, long = 1);
}

using A::f;
using B::f;

void f(int) {} // best  candidate

void g() {
    f(0);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we would return OR_Ambiguous here, and then special case the diagnostic for that when all candidates are redeclarations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have f(0) test in DR tests, but I don't understand how void f(int) {} // best candidate would bring additional coverage. I also don't understand why we'd return OR_Ambiguous.

I think it's worth pointing out that when there is a redeclaration chain, only one declaration out of it is considered a candidate function for overload resolution. At least as far as I saw in the debugger while working on this.

Comment on lines +30 to +49

#define PD_10(x) x, x, x, x, x, x, x, x, x, x,
#define PD_100(x) PD_10(x) PD_10(x) PD_10(x) PD_10(x) PD_10(x) \
PD_10(x) PD_10(x) PD_10(x) PD_10(x) PD_10(x)
#define PD_1000(x) PD_100(x) PD_100(x) PD_100(x) PD_100(x) PD_100(x) \
PD_100(x) PD_100(x) PD_100(x) PD_100(x) PD_100(x)
#define PD_10000(x) PD_1000(x) PD_1000(x) PD_1000(x) PD_1000(x) PD_1000(x) \
PD_1000(x) PD_1000(x) PD_1000(x) PD_1000(x) PD_1000(x)

extern "C" int func3(
PD_10000(int = 0)
PD_10000(int = 0)
PD_10000(int = 0)
PD_10000(int = 0)
PD_10000(int = 0)
PD_10000(int = 0)
PD_10000(int = 0) // expected-error {{too many function parameters; subsequent parameters will be ignored}}
int = 0);

int h = func3();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that really related to the change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intended as a test for the code that emits diagnostics, but I need to update it, yeah.

@Endilll
Copy link
Contributor Author

Endilll commented Jan 30, 2025

During offline discussion with Corentin he pointed out that [over.match.best]/4 talks about multiple declarations that are found, and not simply reachable, like my implementation currently assumes. I'll update it to take the results of name lookup into account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ clang:frontend Language frontend issues, e.g. anything involving "Sema"
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants