-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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
- 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>`_) | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
def err_ovl_ambiguous_default_arg | ||
: Error<"function call relies on ambiguous default argument %select{|for " | ||
"parameter '%1'}0">; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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">; | |
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; | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) {}
There was a problem hiding this comment.
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
.
// 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); | ||
} | ||
} |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (int i = FirstDefaultArgIndex; i <= LastDefaultArgIndex; ++i) { | |
for (int I = FirstDefaultArgIndex; I <= LastDefaultArgIndex; ++I) { |
// [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. | ||
|
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
||
#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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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. |
This patch implements the rule described in N5001.[over.match.best]/4:
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):
(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:
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 timeCheckFunctionCall
is called, number of arguments inCallExpr
is adjusted to include default arguments, so it's too late, too.ConvertArgumentsForCall
seems to be the best fit for this check.