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

hpx::is_trivially_relocatable trait implementation #6264

Merged
merged 28 commits into from
Jul 10, 2023

Conversation

isidorostsa
Copy link
Contributor

This PR deals with the implementation of the is_trivially_relocatable trait. It offers the user an interface to declare their custom class as trivially_relocatable using the macro:

HPX_DECLARE_TRIVIALLY_RELOCATABLE(<class name>)

This assumes that trivially_copyable types are also trivially relocatable, following Folly's convention: https://github.com/facebook/folly/blob/ec297b748575e8ab86333899295715e6e85f909d/folly/Traits.h#L542

@StellarBot
Copy link

Can one of the admins verify this patch?

Comment on lines 21 to 27
struct NotTriviallyCopyable_1
{
NotTriviallyCopyable_1(NotTriviallyCopyable_1 const&){};
};
static_assert(!hpx::is_trivially_relocatable_v<NotTriviallyCopyable_1>,
"Not Trivially Copyable and Not declared Trivially Relocatable type should "
"not be Trivially Relocatable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I recommend

struct NotTriviallyCopyable1 {
    NotTriviallyCopyable1();
    NotTriviallyCopyable1(const NotTriviallyCopyable1&);
    NotTriviallyCopyable1& operator=(const NotTriviallyCopyable1&) = default;
    ~NotTriviallyCopyable1() = default;
};

struct NotTriviallyCopyable2 {
    NotTriviallyCopyable2();
    NotTriviallyCopyable2(const NotTriviallyCopyable2&) = default;
    NotTriviallyCopyable2& operator=(const NotTriviallyCopyable2&);
    ~NotTriviallyCopyable2() = default;
};

struct NotTriviallyCopyable3 {
    NotTriviallyCopyable3();
    NotTriviallyCopyable3(const NotTriviallyCopyable3&) = default;
    NotTriviallyCopyable3& operator=(const NotTriviallyCopyable3&) = default;
    ~NotTriviallyCopyable3();
};

None of these should be trivially relocatable according to P1144. NotTriviallyCopyable2 is controversial but correct: If you ever want to optimize vector::erase or swap_ranges, you can't let trivially relocatable types do anything weird in their assignment operators either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, what could a trivially relocatable type do in the assignment operator to break swap?

Copy link
Contributor

Choose a reason for hiding this comment

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

Literally anything other than "assign the value of the right-hand side to the left-hand side." I'm actually planning a blog post for this week or next, on all the things that potentially go wrong when we treat std::pmr::string as trivially relocatable.

struct Widget {
    std::pmr::string ps;
};

std::vector<Widget> w;
w.push_back(Widget{ std::pmr::string("hello"), &mr1) });
w.push_back(Widget{ std::pmr::string("world"), &mr2) });
w.erase(w.begin()); // ideally should destroy w[0] and relocate downward, right?
assert(w[0].ps.get_allocator().resource() == &mr1); // but today this is required to be true

auto v1 = std::pmr::vector<std::pmr::string>({"hello", "hello"}, &mr1);
auto v2 = std::pmr::vector<std::pmr::string>({"world", "world"}, &mr2);
std::swap_ranges(v1.begin(), v1.end(), v2.begin()); // ideally should swap the bytes, right?
assert(v1[0].get_allocator().resource() == &mr1); // but today this is required to be true

There are several theories of what to do here:

  • Don't call std::pmr::string trivially relocatable at all. (This is the default, implied by the current wording of P1144R8.)
  • Call std::pmr::string trivially relocatable, and put a precondition on all standard library facilities that says you're not allowed to pass them objects from different arenas; if you do, that's library UB. (This is my own preferred theory.)
  • Call std::pmr::string trivially relocatable, and put a precondition on some standard library facilities that says you're not allowed to pass them objects from different arenas; if you do, that's library UB. For example, let's say vector<Widget>::erase gets to assume that all the Widgets in the vector have the same arena, but std::swap<Widget&> does not, and thus simply cannot be optimized for trivially relocatable types. (This is the Bloomberg/P2786R1 theory, as of last week, although it might change in the future.)

Comment on lines 29 to 37
struct NotTriviallyCopyable_2
{
NotTriviallyCopyable_2(NotTriviallyCopyable_2 const&){};
};
HPX_DECLARE_TRIVIALLY_RELOCATABLE(NotTriviallyCopyable_2)

static_assert(hpx::is_trivially_relocatable_v<NotTriviallyCopyable_2>,
"Not Trivially Copyable but declared Trivially Relocatable type should "
"be Trivially Relocatable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I recommend

struct ExplicitlyTriviallyRelocatable1 {
    ExplicitlyTriviallyRelocatable1();
    ExplicitlyTriviallyRelocatable1(const ExplicitlyTriviallyRelocatable1&);
    ExplicitlyTriviallyRelocatable1& operator=(const ExplicitlyTriviallyRelocatable1&);
    ~ExplicitlyTriviallyRelocatable1();
};
HPX_DECLARE_TRIVIALLY_RELOCATABLE(ExplicitlyTriviallyRelocatable1);

struct ExplicitlyTriviallyRelocatable2 {
    ExplicitlyTriviallyRelocatable2();
    ExplicitlyTriviallyRelocatable2(ExplicitlyTriviallyRelocatable2&&);
    ExplicitlyTriviallyRelocatable2& operator=(ExplicitlyTriviallyRelocatable2&&);
    ~ExplicitlyTriviallyRelocatable2();
};
HPX_DECLARE_TRIVIALLY_RELOCATABLE(ExplicitlyTriviallyRelocatable2);

struct DerivedFromExplicitlyTriviallyRelocatable : ExplicitlyTriviallyRelocatable1 {
    ~DerivedFromExplicitlyTriviallyRelocatable();
};

static_assert(hpx::is_trivially_relocatable_v<ExplicitlyTriviallyRelocatable1>);
static_assert(hpx::is_trivially_relocatable_v<ExplicitlyTriviallyRelocatable2>);
static_assert(!hpx::is_trivially_relocatable_v<DerivedFromExplicitlyTriviallyRelocatable>);

Copy link
Contributor Author

@isidorostsa isidorostsa May 30, 2023

Choose a reason for hiding this comment

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

From my understanding, the primary difference between 1 and 2 is that 1 is only copy assignable/constructible while 2 is only move assignable/constructible, with non-trivial functions for everything, making them both not trivially copyable. Does each structure represent a specific edge case scenario that the tests need to cover? I'm asking this to better understand the principles guiding the selection process for future test cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

1 is both copyable and movable; it's a classic C++98 "Rule of Three" type, which you'll probably see a lot of in real code. The fact that when you move it you get a call to the copy constructor (not a separate move constructor) doesn't really matter to me. The point is just that it's not Rule-of-Zero and it isn't move-only.
2 is move-only, exactly isomorphic to unique_ptr.

The point of DerivedFromExplicitlyTriviallyRelocatable is that back when I was first thinking about "The Best Type Traits C++ Doesn't Have," I considered the idea of opting into things like trivial relocatability and trivial comparability by using a member typedef:

struct S {
    using is_trivially_relocatable = void;
};

(The STL uses this technique for is_transparent.) The downside to this is that then anything derived from S inherits that member typedef — i.e., anything derived from a trivially relocatable type ends up being marked as trivially relocatable — which obviously isn't going to work.

Now, neither P1144 nor HPX are actually doing anything at all with member typedefs, so we don't expect anything at all to go wrong with DerivedFromExplicitlyTriviallyRelocatable. But I figured it couldn't hurt to add the test case for it anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, both the tests and the rationale behind not using typedefs make sense, thank you :)

@hkaiser
Copy link
Member

hkaiser commented May 29, 2023

@Quuxplusone thanks for your comments! @isidorostsa if you apply the suggested changes, please stick with our naming conventions, though.

@isidorostsa
Copy link
Contributor Author

@Quuxplusone thanks for your comments! @isidorostsa if you apply the suggested changes, please stick with our naming conventions, though.

Thanks for bringing that up, I looked and found that we require snake_case C++ STL style, is there something else I should keep in mind?

@hkaiser
Copy link
Member

hkaiser commented May 30, 2023

@Quuxplusone thanks for your comments! @isidorostsa if you apply the suggested changes, please stick with our naming conventions, though.

Thanks for bringing that up, I looked and found that we require snake_case C++ STL style, is there something else I should keep in mind?

Please see here for the whole story.

namespace hpx { \
template <> \
struct is_trivially_relocatable<T> : std::true_type \
{ \
Copy link
Contributor Author

Choose a reason for hiding this comment

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

last minute thought: adding static_assert(is_relocatable) ; as it is impossible to be trivially_relocatable but not relocatable.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's what I thought as of P1144R6; but the current revision (P1144R8) adopts the viewpoint that "trivially relocatable" is like "trivially copyable." It implies, "I promise that hypothetically when you (copy|relocate|compare) this thing, you can do it trivially." It doesn't imply anything about whether it is actually okay to (copy|relocate|compare) the thing; that's up to the algorithm's author to check.

E.g.

struct A { int i; A(A&&) = delete; A& operator=(A&&) = delete; };
static_assert(std::is_trivially_copyable_v<A>);  // C++ today
static_assert(std::is_trivially_relocatable_v<A>);  // in P1144R8
A src[10];
A dst[10];
alignas(A) char rawdst[10*sizeof(A)];
std::copy(src, src+10, dst);  // invalid C++ today
std::uninitialized_move(src, src+10, (A*)rawdst);  // also invalid C++ today
std::uninitialized_relocate(src, src+10, (A*)rawdst);  // also invalid in P1144R8

...however, I see that I need to update my libc++ implementation to actually reject the latter. ;)

Anyway, I don't have a strong opinion on which way HPX should define their trait. If you want to make it false for non-relocatable types, that's not crazy; it won't explode or anything. But I did want to point out that P1144R8 chooses to do it differently, and why P1144R8 chooses to do it differently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, I am struggling to think of a case where using std::relocate on a trivially relocatable but not relocatable type would be correct. I think I must be missing something

Copy link
Contributor

Choose a reason for hiding this comment

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

I am struggling to think of a case where using [std::uninitialized_relocate] on a trivially relocatable but not relocatable type would be correct.

That's the point: it'd never be correct. And neither would it be correct to use std::copy on a trivially copyable but not copy-assignable type. That doesn't mean such a type is not trivially copyable (or not trivially relocatable); it just means that you can't use it with those algorithms because they require copy-assignability resp. relocatability.

The interface constraint on std::copy is that the type you pass in needs to be is_copy_assignable. Internally, the optimization into a memcpy kicks in when the type happens to be is_trivially_copyable.

The interface constraint on std::uninitialized_relocate is that the type you pass in needs to be is_relocatable. Internally, the optimization into a memcpy kicks in when the type happens to be is_trivially_relocatable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ohhh I see, so it's just a matter of where this check should be performed, R8 suggests it should not throw a compile error when you give a non-relocatable type this trait, but it should when we try to relocate it, right? And this is checked with a static assertion inside the relocation algorithms

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I believe you've got it now. Here's a concrete example with libc++: https://godbolt.org/z/Yr9MdncoT

struct S {
    const std::unique_ptr<int> m;
};

static_assert(std::is_trivially_relocatable_v<S>);
static_assert(!std::is_relocatable_v<S>);

void f(S *p) {
    std::relocate_at(p, p); // ill-formed
}

__memory/relocate_at.h:91:19: error: static assertion failed [...]

If you could relocate S, then you could do so trivially. But in fact you can't relocate S.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

| If you could relocate S, then you could do so trivially. But in fact you can't relocate S.

Just for a sanity check; arrays of trivially relocatable types T[] should be considered trivially relocatable, but not relocatable (as not move constructable)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just for a sanity check; arrays of trivially relocatable types T[] should be considered trivially relocatable, but not relocatable (as not move constructable)?

Yes, that's exactly right. https://godbolt.org/z/9n4YGTjfd (P1144R8 has actually removed std::is_relocatable_v, but kept std::relocatable. My Godbolt implementation keeps both, at least for now, since they're trivial to implement.)

@isidorostsa isidorostsa marked this pull request as ready for review June 15, 2023 16:37
@isidorostsa
Copy link
Contributor Author

isidorostsa commented Jun 15, 2023

@hkaiser

  • I applied the styling changes and east const to the tests of is_relocatable as well
  • Still need to move this whole implementation over to type_support, could you please verify that as a sanity check?

Copy link
Member

@hkaiser hkaiser left a comment

Choose a reason for hiding this comment

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

Let's have these low-level facilities in core/type_support after all (as discussed). Otherwise we will not be able to use this for the implementation of our own data types (small_vector, etc.).

// All trivially copyable types are trivially relocatable
template <typename T>
struct is_trivially_relocatable<T,
std::enable_if_t<std::is_trivially_copyable_v<T>>> : std::true_type
Copy link
Member

@hkaiser hkaiser Jun 22, 2023

Choose a reason for hiding this comment

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

Do we have to disable this specialization if T was marked as trivially relocatable (by specializing is_trivially_relocatable<T>) where T is also trivially copyable?

Alternatively, we could change the macro above to:

#define HPX_DECLARE_TRIVIALLY_RELOCATABLE(T)                                   \
    namespace hpx {                                                            \
        template <>                                                            \
        struct is_trivially_relocatable<T,                                     \
            std::enable_if_t<!std::is_trivially_copyable_v<T>>>                \
          : std::true_type                                                     \
        {                                                                      \
        };                                                                     \
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, just change the base case to

    template <typename T, typename = void>
    struct is_trivially_relocatable : std::is_trivially_copyable<T>
    {
    };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you that's very elegant, I will apply this. Would

std::enable_if_t<!std::is_trivially_relocatable_v>> ...

work as a more general solution? It does look kind of suspicious...

Copy link
Contributor

Choose a reason for hiding this comment

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

No, you don't want to define is_trivially_relocatable in terms of itself. ;)
Notice that P1144 can't help with this part of the HPX design, because P1144 proposes that the core language should just know which types are trivially relocatable — P1144's raison d'etre is specifically to avoid this kind of opt-in macro. (But HPX needs this part because P1144's core-language changes aren't in the language yet.)

@isidorostsa: You should definitely take a look at what BSL and Folly are doing with their opt-in macros, and try to evaluate what advantages and disadvantages each approach might have. I think what you're doing with just letting the user specialize hpx::is_trivially_relocatable<T, enable_if_t<...>> is fine, but I don't think it is isomorphic to either BSL's or Folly's approach, so it would be good to evaluate, justify, and document.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this note, having examined bslmf and folly we decided to avoid using a nested trait, due to it being invasive to existing code to work. We did implement more generic macros to compensate for the lack of template and conditional support by the previous macro.

new macros: 7139cb1

* Support for incomplete types, and conditionality

* Added variadic template macros and tests for them
Copy link
Member

@hkaiser hkaiser left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@hkaiser
Copy link
Member

hkaiser commented Jul 10, 2023

bors merge

@bors
Copy link

bors bot commented Jul 10, 2023

Build succeeded!

The publicly hosted instance of bors-ng is deprecated and will go away soon.

If you want to self-host your own instance, instructions are here.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

@bors bors bot merged commit 8b4f11d into STEllAR-GROUP:master Jul 10, 2023
16 of 17 checks passed
@isidorostsa isidorostsa deleted the relocate branch August 6, 2023 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants