[PATCH] D96832: [Clang][Attributes] Allow not_tail_called attribute to be applied to virtual function.

Arthur O'Dwyer via Phabricator via cfe-commits cfe-commits at lists.llvm.org
Thu Feb 18 07:10:51 PST 2021


Quuxplusone added inline comments.


================
Comment at: clang/include/clang/Basic/AttrDocs.td:4109
-      [[clang::not_tail_called]] int foo2() override;
-    };
   }];
----------------
aaron.ballman wrote:
> ahatanak wrote:
> > Quuxplusone wrote:
> > > (Moving into a thread)
> > > 
> > > > This patch doesn't prevent the call to method in the code below from being tail called,
> > > > but I suppose users would expect the attribute to prevent the tail call?
> > > ```
> > > struct B {
> > >   virtual void method();  
> > > };
> > > struct D : B {
> > >   [[clang::not_tail_called]] void method() override; 
> > > };
> > > ```
> > > 
> > > The way virtual calls are handled in C++ is, all attributes and properties of the call are determined based on the //static// type at the call site; and then the //runtime destination// of the call is determined from the pointer in the vtable. Attributes and properties have no runtime existence, and so they physically cannot affect anything at runtime. Consider https://godbolt.org/z/P3799e :
> > > 
> > > ```
> > > struct Ba {
> > >   virtual Ba *method(int x = 1);  
> > > };
> > > struct Da : Ba {
> > >   [[clang::not_tail_called]] [[nodiscard]] Da *method(int x = 2) noexcept override; 
> > > };
> > > auto callera(Da& da) {
> > >     Ba& ba = da;
> > >     ba.method();
> > > }
> > > ```
> > > Here the call that is made is a //virtual// call (because `Ba::method` is virtual); with a default argument value of `1` (because `Ba::method`'s `x` parameter has a default value of `1`); and it returns something of type `Ba*` (because that's what `Ba::method` returns); and it is not considered to be noexcept (because `Ba::method` isn't marked noexcept); and it's okay to discard the result (because `Ba::method` is not nodiscard) and it is tail-called (because `Ba::method` doesn't disallow tail calls). All of these attributes and properties are based on the //static// type of variable `ba`, despite the fact that //at runtime// we'll end up jumping to the code for `Da::method`. According to the source code, statically, `Da::method` has a default argument of `2`, returns `Da*`, is noexcept, and is nodiscard, and disallows tail-calls. But we're not calling `da.method()`, we're calling `ba.method()`; so none of that matters to our call site at `callera`.
> > > 
> > > I think this patch is a good thing.
> > OK, I see. I think this patch is fine then.
> > 
> > Should we add an explanation of how virtual functions are handled? The doc currently just says the attribute prevents tail-call optimization on statically bound calls.
> > I think this patch is a good thing.
> 
> I agree with your explanation but I'm not certain I agree with your conclusion. :-) I think the examples you point out are more often a source of confusion for users than not because of the nature of static vs dynamic dispatch, and so saying "this behavior can be consistent with all of these other things people often get confused by" may be justifiable but also seems a bit user-hostile.
> 
> Taking a slightly different example:
> ```
> struct Ba {
>   [[clang::not_tail_called]] virtual Ba *method();  
> };
> struct Da : Ba {
>   Ba *method() override; 
> };
> 
> void callera(Da& da) {
>     Ba& ba = da;
>     ba.method();
> }
> ```
> There's no guarantee that `Ba::method()` and `Da::method()` have the same not-tail-called properties. The codegen for this case will still attach `notail` to the call site even though the dynamic target may not meet that requirement.
> 
> tl;dr: I think `notail` is a property of the call expression and the only way to know that's valid is when you know what's being called, so the current behavior is more user-friendly for avoiding optimization issues. I'd prefer not to relax that unless there was a significantly motivating example beyond what's presented here so far.
> saying "this behavior can be consistent with all of these other things people often get confused by" may be justifiable but also seems a bit user-hostile.

I disagree. In areas where (we agree that) users are already a bit confused, I want to treat consistency-of-interface as a virtue. Imagine a student being confused for weeks about the behavior of attributes on virtual methods — "I put `[[nodiscard]]`/`[[noreturn]]`/`[[deprecated]]` on the child method, but the compiler isn't warning me when I call the parent method!" — and then //finally// someone asks him to repeat it slowly, and the lightbulb goes on: "Oh, right, I'm calling the //parent// method..." So now he "gets it." Oh, except, the entire mental model breaks down again for the `[[not_tail_called]]` attribute, because that attribute uses different rules.

Let's just skip that very last step. Let's have all attributes use the same rules, so that the mental model for one carries over to all the others.

Btw, here's all the interesting attributes/properties/bells/whistles I was able to think of: https://godbolt.org/z/3PYe87 (Looks like Clang is more permissive than it should be with covariant return types.) It'd be interesting to see what a linter like PVS-Studio thinks of all these overriders. I hope it would catch m9 and m10 at least.

I would support (but not myself attempt to implement) `-Wall` warnings for m3/m4, for m9, and for m5/m6/m11/m12.


Repository:
  rG LLVM Github Monorepo

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D96832/new/

https://reviews.llvm.org/D96832



More information about the cfe-commits mailing list