Skip to content
New issue

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

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

Already on GitHub? # to your account

Allow pass-by-value for classes with deleted copy-constructor #14426

Conversation

vepadulano
Copy link
Member

This fixes #14425 (the reproducer is added as test)

Note that the patch comes straight from upstream cppyy https://github.com/wlav/cppyy-backend/blob/25caf988cef1f2f76705c07b7262f076e8ed0e01/cling/src/core/metacling/src/TClingCallFunc.cxx#L468-L485 even though it's a patch in TCling. I open this PR as a draft to start the discussion as to how we can integrate this change, since it's necessary to fix a bug that also affects usage of ROOT classes via PyROOT.

@vepadulano vepadulano self-assigned this Jan 24, 2024
@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on windows10/default.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Failing tests:

Copy link

github-actions bot commented Jan 25, 2024

Test Results

    10 files      10 suites   2d 14h 3m 13s ⏱️
 2 498 tests  2 442 ✅ 0 💤  56 ❌
23 877 runs  23 403 ✅ 0 💤 474 ❌

For more details on these failures, see this check.

Results for commit e910a1c.

♻️ This comment has been updated with latest results.

// copy constructor exists, so check for the most common case: the trivial
// one, but not uniquely available, while there is a move constructor.
CXXRecordDecl* rtdecl = QT->getAsCXXRecordDecl();
if (rtdecl && (rtdecl->hasTrivialCopyConstructor() && !rtdecl->hasSimpleCopyConstructor()) \
Copy link
Member

Choose a reason for hiding this comment

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

Why require rtdecl->hasTrivialCopyConstructor()? AFAICT it should be sufficient to check !rtdecl->hasSimpleCopyConstructor()... (what does rt stand for? There is no t in CXXRecordDecl... IIRC the usual acronym is RD in upstream Clang code)

Also, please remove the final \, the line isn't in a macro...

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to check if the changes from cppyy worked as-is, it seems like they do (except for Windows for now). Your suggestions are very valid and I will include them in the next changes. But I see no complaint about the overall direction of the PR, so I'm glad about that!

Copy link
Member

Choose a reason for hiding this comment

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

We will probably need a unittest for callfunc as that’s a critical component for root.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, I'll try to come up with something that is equivalent to what PyROOT tries to do in this case.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have included a first test, based on already available tests for TClingCallFunc. Let me know what you think

Copy link
Member Author

Choose a reason for hiding this comment

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

It seems that the currently failing tests (e.g. here) are due to the missing hasTrivialCopyConstructor check, I will reintroduce it in a new commit

Copy link
Member

Choose a reason for hiding this comment

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

Whatever we do we should do it in sync with upstream otherwise it will be a nightmare to upgrade. If the patch differs we should propose it upstream, if it is only the test we should propose the test. That investment will make our lives easier in near future.

Copy link
Member Author

Choose a reason for hiding this comment

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

In principle we can pick what upstream does literally as-is (if we can afford living with the \ character in the if condition). It is not only the test clearly, as this feature was never present before in ROOT's TClingCallFunc. The main issue I see with picking upstream as-is is that there is no way to pinpoint the necessary commits, the whole TClingCallFunc::make_narg_call belongs to a single commit when the fork of ROOT was started https://github.com/wlav/cppyy-backend/blame/25caf988cef1f2f76705c07b7262f076e8ed0e01/cling/src/core/metacling/src/TClingCallFunc.cxx#L478 . I have no problem with copying upstream code verbatim though. Let me know what I should do because this problem is quite visible in experiment code using PyROOT.

Copy link
Member

Choose a reason for hiding this comment

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

This is one of the situations where we might not be able to. The CallFunc changes are only tested within cppyy context and that might fall short or the changes might cause regressions. We need to re-integrate the TClingCallFunc changes very carefully. We can't really copy verbatim in some cases, especially the lambda support as that I think has a patch in clang.

In an ideal world (with CppInterOp) these changes will be in one place but until then I am not sure if there is a single best go-to strategy.

Let me try to summon @wlav.

@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on ROOT-ubuntu2204/nortcxxmod.
Running on root-ubuntu-2204-2.cern.ch:/home/sftnight/build/workspace/root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on windows10/default.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on windows10/default.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on ROOT-ubuntu2204/nortcxxmod.
Running on root-ubuntu-2204-2.cern.ch:/home/sftnight/build/workspace/root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

Build failed on windows10/default.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Failing tests:

@vepadulano vepadulano force-pushed the rntuple-fix-pyroot-model-create branch from 60e6526 to 2c70368 Compare January 30, 2024 14:12
@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@vepadulano vepadulano marked this pull request as ready for review January 30, 2024 16:48
@vepadulano
Copy link
Member Author

After a debugging session with @bellenot , we have found out that CXXRecordDecl created for a std::unique_ptr on Windows will say that RD->hasMoveConstructor() is false, which imho is astonishing. We could surely introduce a small patch to cover the unique_ptr use case for Windows, but that will never scale to all classes that have a deleted copy constructor. Since this is blocking an experiment framework from moving forward, the support for Windows is not crucial, so in the latest commit I decided to remove the change on Windows builds.

@vgvassilev The change is taken from cppyy's patches, but as you also suggested it's not trivial to just take all the code verbatim. So I restricted it to the minimum amount of changes that apply for the reported use case. Let me know how to make progress here.

@hahnjo
Copy link
Member

hahnjo commented Jan 30, 2024

After a debugging session with @bellenot , we have found out that CXXRecordDecl created for a std::unique_ptr on Windows will say that RD->hasMoveConstructor() is false, which imho is astonishing. We could surely introduce a small patch to cover the unique_ptr use case for Windows, but that will never scale to all classes that have a deleted copy constructor.

I believe it would be better to understand what's going on here. From a quick look into the Clang sources, it seems there is some special handling if the (move) constructor is templated. And indeed, this seems to be one difference between Microsoft's STL on the one hand and libc++ and libstdc++ on the other: Microsoft's STL only has templated move constructors while the other two have a "default" version for matching types. If this hypothesis is correct, it would mean that this patch wouldn't work on user-defined types either with only a templated move constructor...

@vgvassilev
Copy link
Member

After a debugging session with @bellenot , we have found out that CXXRecordDecl created for a std::unique_ptr on Windows will say that RD->hasMoveConstructor() is false, which imho is astonishing.

Have we called Sema::ForceDeclarationOfImplicitMembers(RD) to force the compiler to synthesize the implicit data members?

@vepadulano
Copy link
Member Author

If this hypothesis is correct, it would mean that this patch wouldn't work on user-defined types either with only a templated move constructor...

Interesting! This is something I can test :)

@vepadulano
Copy link
Member Author

So indeed this other test does not work

import ROOT

ROOT.gInterpreter.Declare(r'''
struct A{
    int mA{42};
    A() {}
    A(const A&) = delete;
    template<typename T = int>
    A(A &&) {}
};
int foo(A a = A{}) { return a.mA; }
''')

print(ROOT.foo())
input_line_36:18:45: error: call to deleted constructor of 'A'
         new (ret) (int) (((int (&)(A))foo)(*(A*)args[0]));
                                            ^~~~~~~~~~~~
input_line_35:5:5: note: 'A' has been explicitly marked deleted here
    A(const A&) = delete;
    ^

Also, it seems to me that indeed in the case of a templated move constructor, the AST generated by clang simply does not report it in the class definition data, see this example. There, by commenting the template declaration of the move constructor, one can see the corresponding AST definition data being filled, i.e. from

| | |-MoveConstructor

to

| | |-MoveConstructor exists non_trivial user_declared

I am surely not expert enough in this area, but I noticed that when the constructor is templated the corresponding CXXConstructorDecl is nested inside a FunctionTemplateDecl, so that may be "hiding" it somehow?

@vepadulano
Copy link
Member Author

Have we called Sema::ForceDeclarationOfImplicitMembers(RD) to force the compiler to synthesize the implicit data members?

Yes I tried also that and doesn't change the situation I described in the post above.

         if (RD) {
            clang::Sema &S = fInterp->getSema();
            S.ForceDeclarationOfImplicitMembers(RD);
         }

         if(RD && (RD->hasTrivialCopyConstructor() && !RD->hasSimpleCopyConstructor()) && RD->hasMoveConstructor()) {
            // move construction as needed for classes (note that this is implicit)
            callbuf << "std::move(*(" << type_name.c_str() << "*)args[" << i << "])";
         } else {
            // otherwise, and for builtins, use copy construction of temporary*/
            callbuf << "*(" << type_name.c_str() << "*)args[" << i << "]";
         }

@vgvassilev
Copy link
Member

Can you paste the entire callfunc wrapper?

@hahnjo
Copy link
Member

hahnjo commented Jan 31, 2024

Also, it seems to me that indeed in the case of a templated move constructor, the AST generated by clang simply does not report it in the class definition data, see this example. There, by commenting the template declaration of the move constructor, one can see the corresponding AST definition data being filled, i.e. from

| | |-MoveConstructor

to

| | |-MoveConstructor exists non_trivial user_declared

I am surely not expert enough in this area, but I noticed that when the constructor is templated the corresponding CXXConstructorDecl is nested inside a FunctionTemplateDecl, so that may be "hiding" it somehow?

I suppose that with a template move constructor is not the "canonical form" of a move constructor (or whatever the right standardese is). I was hoping that it's reported once the template is instantiated, but that doesn't seem the case either.

In any case, what you really want to ask Clang is "can you copy-construct this thing" and "can you move-construct this thing". That logic is implemented in EvaluateBooleanTypeTrait of SemaExprCXX.cpp and is indeed a bit hairy because it actually has to run through all the machinery of template instantiation and overload resolution. You can invoke that machinery via Sema::BuildTypeTrait with Kind = clang::TT_IsConstructible. Alternatively, you can try using the __is_constructible builtin in the generated wrapper code, but I'm not sure how that would work because it's not available in the preprocessor...

Final remark: A better heuristic could be to check for defaultedCopyConstructorIsDeleted() and then just try the std::move. I think that heuristic still has false-negatives and false-positives, but I think it gets the one case right where the explicit intent is to delete the copy constructor...

@vepadulano
Copy link
Member Author

Thanks @hahnjo for the valuable input! I will try to digest SemaExprCXX.cpp.

Unfortunately

RD->defaultedCopyConstructorIsDeleted(): 0

So it seems I can't use that method to distinguish this case either...

In creating the expression to JIT-compile for calling a certain method with arguments, TClingCallFunc takes each argument (type-erased, then casted to the right type) by const-ref. Practically, this pattern prevents pass-by-value semantics on the PyROOT side for classes with a deleted copy-constructor, e.g. std::unique_ptr. This patch aims at adding an automatic std::move when we can detect that the class does not provide a copy constructor. This logic is applied with two different conditions that serve different use cases:
* If the class does not have a simple copy constructor but does have a trivial one and also has a move constructor. This is the case for `std::unique_ptr` of libc++ and libstdc++.
* If the first condition does not apply, we traverse the available constructors of the class, find the copy constructor, check whether that is deleted. If so, we automatically inject the `std::move`. This is the case of `std::unique_ptr` of the Microsoft STL, which is different since its move constructor is templated. In this case, clang cannot completely identify the move constructor in the AST. By extension this applies to any class with a templated move constructor and a deleted copy constructor.

Note that the changes in this commit began from the patched version of TClingCallFunc available in cppyy https://github.com/wlav/cppyy-backend/blob/25caf988cef1f2f76705c07b7262f076e8ed0e01/cling/src/core/metacling/src/TClingCallFunc.cxx#L468-L485

Co-authored-by: Wim Lavrijsen <WLavrijsen@lbl.gov>
@vepadulano vepadulano force-pushed the rntuple-fix-pyroot-model-create branch from 2c70368 to 1c7a439 Compare February 1, 2024 00:18
@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

Test that we automatically inject an `std::move` in the TClingCallFunc wrapper call in cases where a copy constructor is not available for the class.
@vepadulano vepadulano force-pushed the rntuple-fix-pyroot-model-create branch from 1c7a439 to e910a1c Compare February 1, 2024 06:32
@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@vgvassilev
Copy link
Member

We probably need to more work to collect the information about the templated ctor. I'd propose to do Sema::LookupConstructors and iterate over the constructor list. If you see a FunctionTemplateDecl then you can do to get the getUnderlyingDecl and cast it to a CXXConstructorDecl. Note that it seems in C++ a ctor template cannot be a default ctor or a copy ctor: http://eel.is/c++draft/class.ctor

@vepadulano
Copy link
Member Author

Superseded by #17030

@vepadulano vepadulano closed this Nov 30, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PyROOT calls into deleted copy-constructor in valid C++ scenarios
4 participants