hsutter / cppfront

A personal experimental C++ Syntax 2 -> Syntax 1 compiler
Other
5.24k stars 223 forks source link

Move from last-use causes problems with RAII types, and types with member functions with side effects #1002

Closed bluetarpmedia closed 3 months ago

bluetarpmedia commented 4 months ago

Describe the bug A combination of UFCS and std::move introduced by a variable's last-use causes a C++ compiler error.

To Reproduce Run cppfront on this code:

main: () -> int = {

    m : std::mutex = ();
    cv : std::condition_variable = ();

    (copy lk: std::unique_lock = (m))
    {
        cv.wait(lk, :() -> bool = true);
    }

    _ = m.native_handle();  // Workaround for bug #968

    return 0;
}

After compiling with clang it results in an error:

error: no matching function for call to object of type '(lambda at main.cpp2:8:9)'
    8 |         CPP2_UFCS(wait)(std::move(cv), std::move(lk), []() mutable -> bool { return true;  });
      |         ^~~~~~~~~~~~~~~

One workaround is to add something after the cv.wait line that becomes the last use of lk:

_ = lk.mutex();

Repro on Godbolt

hsutter commented 3 months ago

Thanks! Ah, this is similar to #999, except the key thing is that lk is not the first argument (before .), it's another argument.

[Updated to add:] It's actually intentional that this is an error. Briefly, the reason is that if a definite last use of a local object modifies that object, then the code is not looking at the object again (because by definition it's a last use) and therefore trying to implicitly discard the new value. For a complete discussion, please see Design note: Explicit discard.

Like #999, this is still improved by the recent commit 70419944c4ba9166ac6b479cd6eef0ca410235a3, but instead of the nicer error message #999 gets, currently this example gets this:

demo.cpp2(7): error C2338: static_assert failed: 'this function call syntax tries 'obj.func(...)', then 'func(obj,...);' - both failed, did you spell the function name correctly?'

And on MSVC that's followed by this, which gives the right clue never the end:

demo.cpp2(8): note: see reference to function template instantiation 'decltype(auto) main::<lambda_1>::operator ()<std::condition_variable,std::unique_lock<std::mutex>,main::<lambda_2>,false>(Obj &&,std::unique_lock<std::mutex> &&,main::<lambda_2> &&) noexcept(false) const' being compiled
        with
        [
            Obj=std::condition_variable
        ]
demo.cpp2(8): error C2660: 'std::condition_variable::wait': function does not take 2 arguments
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.38.33130\include\mutex(561): note: see declaration of 'std::condition_variable::wait'
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.38.33130\include\mutex(567): note: could be 'void std::condition_variable::wait(std::unique_lock<std::mutex> &,_Predicate)'
demo.cpp2(8): note: 'initializing': cannot convert from 'std::unique_lock<std::mutex>' to 'std::unique_lock<std::mutex> &'
demo.cpp2(8): note: A non-const reference may only be bound to an lvalue
demo.cpp2(8): note: while trying to match the argument list '(std::unique_lock<std::mutex>, main::<lambda_2>)'
demo.cpp2(8): error C3861: 'wait': identifier not found
demo.cpp2(8): note: 'wait': function was not declared in the template definition context and can be found only via argument-dependent lookup in the instantiation context

So that's better than above, but not as nice as for the first argument before .. Let me see if I can make this message better...

hsutter commented 3 months ago

Based on this feedback, I think the first error message should be:

demo.cpp2(8): error C2338: static_assert failed: 'this function call syntax tries 'obj.func(...)', then 'func(obj,...);', but both failed - if this function call is passing a local variable that will be modified by the function, but that variable is never used again in the function so the new value is never used, that's likely the problem - if that's what you intended, add another line '_ = obj;' afterward to explicitly discard the new value of the object'

That's at least a clearer clue to the programmer.

Thanks!

bluetarpmedia commented 3 months ago

I may be missing something but I don't think the issue is just that the Cppfront error could be clearer?

Here's a fuller example: Godbolt

See lines 20 and 42. How should this code be changed to fix the error?

EDIT - I saw from the suggestion to add the _ = obj; statement. So the relevant snippet would become:

(copy lk: std::unique_lock = (m))
{
    cv.wait(lk, :() -> bool = processed);

    _ = lk;  // <-- New
}

Similar to my RAII example in #999, I find this awkward. IMO the code is clear that the condition_variable wait has a side-effect on the lock, so it seems surprising that I have to discard the lock's new value, because the new value was in fact used -- just not by me.

hsutter commented 3 months ago

After thinking about this, I think this is about "guard" objects. Those are the local objects whose values are used "by someone else than this function's remaining body code" after their definite last use because:

... and so I think those are the only cases? I can't think of another case where code could consume the new value of a modified-at-last-use local object.

Next, what to do about it? At minimum we could just teach people who use local synchronization variables that this is how to use them in Cpp2. Or... we could add some way to declare guards (possibly even by recognizing the prefix guard in the variable's name as a meaningful convention) and then Cpp2 would know not to move from last use.

DyXel commented 3 months ago

Its a tricky one, it seems std::move from definite last use inherently conflicts with the RAII helpers. Not a big fan of "(possibly even by recognizing the prefix guard in the variable's name as a meaningful convention)", doesn't seem general enough. I thought about it for days, and I've got some ideas:

EDIT: Silly me, you can't grab the pointer of a rvalue... This happens to work because std::move is not emitted when doing the above. Still to be confirmed if that's a coincidence, a bug, or intended behavior 😅. On the bright side, in that case dangling isn't possible.

bluetarpmedia commented 3 months ago

Should #999 be merged with this issue? They’re not duplicates but I think both are symptoms of a bigger, parent issue.

I think there may be more cases than just guard objects to consider.

Here are a few that I’m wondering about (but haven’t tested yet):

DyXel commented 3 months ago

Yes, I think we should try to merge them both, its not about UFCS per se. It's about not giving up one of the main strengths of RAII (I pass you this value, you do the cleanup for me, regardless of how we exit the scope).

JohelEGP commented 3 months ago

Still to be confirmed if that's a coincidence, a bug, or intended behavior

Intended. See https://github.com/hsutter/cppfront/issues/558#issuecomment-1715765151.

hsutter commented 3 months ago

Yes, I agree this isn't about UFCS -- would you like to update the issue titles to reflect that?

gregmarr commented 3 months ago

What if we had a way to say this?

"The destructor of this type is always the definite last use of an object of this type, no matter what inout this functions are called on it during its lifetime."

That would take care of one group of issues.

Another group of issues is this.

"This function has side effects so we need it to not be const, but these side effects are not observable on the object itself, so it's okay that the caller doesn't read from this object again later to observe them."

Is that an accurate statement of the group of issues? Do we need a decoration for this that indicates this class of behavior?

bluetarpmedia commented 3 months ago

Is that an accurate statement of the group of issues?

Yes, I think you nicely summarised the issues. After thinking about this further I prefer the idea of opting-in to the "move from last-use" behaviour, for the reasons I outlined in #1030.

One major point, for me, is that I want to use my existing C++ types (e.g. RAII types, or code that I'm slowly migrating to Cpp2) which I can't modify with a metafunction or a new decoration for this.

hsutter commented 3 months ago

Thanks! See #1030, this should now be fixed in the above commit.

The original code example now works. (Unrelatedly, making the code work on MSVC also requires removing the native_handle call, because evidently the MS STL std::mutex doesn't have that member, I don't know why; but that's unrelated to this issue.)

bluetarpmedia commented 3 months ago

Interesting, I just learned that the presence of std::mutex::native_handle is implementation-defined (not just its behaviour). Anyway, that line with native_handle was only a workaround for #968.

hsutter commented 3 months ago

TIL:

the presence of std::mutex::native_handle is implementation-defined (not just its behaviour)

🧑‍🎓

Thanks for sharing that info!