-
Notifications
You must be signed in to change notification settings - Fork 14.9k
[libc++] Ensure that std::expected
has no tail padding
#69673
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
[libc++] Ensure that std::expected
has no tail padding
#69673
Conversation
Some questions / notes:
|
Also, since this would be an ABI break, I guess there is a bit more process involved to get something like this in? |
GNU anonymous struct members seem to work great for this use case: #include <memory>
//
struct bp {
private:
alignas(4) bool b_;
};
static_assert(sizeof(bp) == 4);
static_assert(std::__libcpp_datasizeof<bp>::value == 1);
//
struct s1 {
s1() : bp_{}, b_{} {};
private:
[[no_unique_address]] bp bp_;
bool b_;
};
static_assert(sizeof(s1) == 4);
static_assert(std::__libcpp_datasizeof<s1>::value == 2); // bad :(
//
struct s2 {
s2() : bp_{}, b_{} {};
private:
struct {
[[no_unique_address]] bp bp_;
bool b_;
};
};
static_assert(sizeof(s2) == 4);
static_assert(std::__libcpp_datasizeof<s2>::value == 4); // good :) https://godbolt.org/z/EenY54E4r Sadly this is not portable... |
It seems that when Does an unnamed bit-field work? |
It looks like it does! struct s3 {
s3() : bp_{}, b_{} {};
private:
[[no_unique_address]] bp bp_;
bool b_;
int : 0;
};
static_assert(sizeof(s3) == 4);
static_assert(std::__libcpp_datasizeof<s3>::value == 4); // good :) |
libcxx/include/__expected/expected.h
Outdated
_LIBCPP_NO_UNIQUE_ADDRESS __union_t<_Tp, _Err> __union_; | ||
bool __has_val_; | ||
}; | ||
return sizeof(__calc_expected) - __libcpp_datasizeof<__calc_expected>::value; |
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.
Why can't we just use sizeof(expected) - __libcpp_datasizeof<expected>::value
?
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.
At this point the full definition of expected
is not available yet, so I'm getting errors like:
__expected/expected.h:960:12: error: invalid application of 'sizeof' to an incomplete type 'expected<TracedBase<false, false>,
TracedBase<true>>'
# | 960 | return sizeof(expected) - __libcpp_datasizeof<expected>::value;
# | | ^~~~~~~~~~~~~~~~
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 seems we can determine the size of the padding via only sizeof(__union_t<_Tp, _Err>)
and __libcpp_datasizeof<__union_t<_Tp, _Err>>::value
without inventing a new type.
libcxx/include/__expected/expected.h
Outdated
_LIBCPP_NO_UNIQUE_ADDRESS __union_t<_Tp, _Err> __union_; | ||
bool __has_val_; | ||
char __padding_[__calculate_padding()]{}; |
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.
Could we try to allow tail padding inside expected
if the subobject didn't have any? e.g. it would be fine if expected<int, int>
has tail padding, since the subobject can't overwrite any of the expected
tail padding.
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.
I zero initialize the new padding arrays (otherwise the compiler generated copy constructors would be UB I guess?). The bytes in those arrays might get overwritten by subsequent constructor/destructor calls in methods like emplace() but this should be OK.
Ah, good point. Maybe worth a one-line comment somewhere since I was actually about to ask a question about that.
The test(s) that assert
sizeof(expected) == datasizeof(expected)
should probably live in new files, maybe calledtest/std/utilities/expected/expected.expected/datasize.pass.cpp
andexpected.void/datasize.pass.cpp
?
They should be in libcxx/test/libcxx/<...>
since that is where we store our libc++ specific tests.
libcxx/include/__expected/expected.h
Outdated
@@ -955,8 +956,17 @@ class expected { | |||
_LIBCPP_NO_UNIQUE_ADDRESS _ErrorType __unex_; | |||
}; | |||
|
|||
_LIBCPP_HIDE_FROM_ABI static constexpr auto __calculate_padding() { |
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.
This won't work if there is no tail padding, since we'll end up creating a char __padding_[0];
below. This is the case for e.g. std::expected<char, char>
. Needs tests.
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.
I implemented an alternative approach to calculate the padding type that avoids zero-length arrays (still wondering if there is a more elegant way, though). I also added some tests.
We could consider adding a Clang extension to support this case, to avoid the ABI change here and maintain the compact layout. Would that be useful to libc++, or would you need (at least) for GCC to implement the same extension before you could make use of it? |
I couldn't get the unnamed bit-field idea to work sadly. I couldn't reliably calculate the needed bits in |
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.
I don't think that filling in the tail padding is sufficient here. Once we fix Clang to properly implement the "transparent replacement" rule for std::construct_at
, constant evaluation of expected::emplace
is going to stop working, and this change doesn't help there.
If you don't want to rely on a language extension, the rule you need to conform to is that you can't transparently replace a potentially overlapping subboject -- you can't use construct_at
on a base class or a [[no_unique_address]]
member.
Here's one approach you could take:
... then apply |
I had more thought about it and still a bit unsure about manually adding Imagine one day we have Although this is a hypothetical problem which does not exist yet, but things like this could exist somewhere and we are just not aware of. Had discussion with Louis today and he had an idea like this
This way we can achieve |
@zygoloid s |
I had a shot at the The only downside I see is that
Wouldn't |
So right now, this test fails: |
Yes, that would be invalid. You need to apply |
Ahh, so when ~repr() {
if (__has_val_) {
std::destroy_at(std::addressof(__union_.__val_));
} else {
std::destroy_at(std::addressof(__union_.__unex_));
}
} |
I've tried to implement the " Internally, the |
It seems that such strategy will make the tail padding of Perhaps we should use a mixed strategy:
It's painful that we have no |
libcxx/include/__expected/expected.h
Outdated
_LIBCPP_NO_UNIQUE_ADDRESS __union_t<_Tp, _Err> __union_; | ||
bool __has_val_; | ||
struct __expected_repr { | ||
_LIBCPP_HIDE_FROM_ABI constexpr explicit __expected_repr() = delete; |
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.
Why do we need an overload set here? If we remove that and simply directly initialize the members that would make this thing a lot simpler.
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 you mean the constructor overload set? I thought since __repr
needs a destructor anyways I'd create some constructors for easier use and more foolproof initialization of the __has_val_
flag. But those are not strong feelings.
What can certainly be removed from __repr
and union constructors are the std::__expected_construct_in_place_from_invoke_tag
/std::__expected_construct_unexpected_from_invoke_tag
overloads. What do you think?
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.
What can certainly be removed from
__repr
and union constructors are thestd::__expected_construct_in_place_from_invoke_tag
/std::__expected_construct_unexpected_from_invoke_tag
overloads. What do you think?
Well, that didn't work: It seems the function really has to be threaded down into the union and only std::invoke
d down there...
# | In file included from /freebsd/data/linux/git-net/llvm-project/libcxx/test/std/utilities/expected/expected.expected/monadic/transform.pass.cpp:22:
# | In file included from /freebsd/data/linux/git-net/llvm-project/build/include/c++/v1/expected:44:
# | /freebsd/data/linux/git-net/llvm-project/build/include/c++/v1/__expected/expected.h:837:11: error: call to deleted constructor of 'NonCopy'
# | 837 | : __val_(std::forward<_Args>(__args)...) {}
# | | ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
# | /freebsd/data/linux/git-net/llvm-project/build/include/c++/v1/__expected/expected.h:906:11: note: in instantiation of function template specialization 'std::expected<NonCopy, int>::__union
_t<NonCopy, int>::__union_t<NonCopy>' requested here
# | 906 | : __union_(__tag, std::forward<_Args>(__args)...), __has_val_(true) {}
# | | ^
# | /freebsd/data/linux/git-net/llvm-project/build/include/c++/v1/__expected/expected.h:173:9: note: in instantiation of function template specialization 'std::expected<NonCopy, int>::__repr::
__repr<NonCopy>' requested here
# | 173 | : __repr_(in_place, std::invoke(std::forward<_Func>(__f), std::forward<_Args>(__args)...)) {}
# | | ^
# | /freebsd/data/linux/git-net/llvm-project/build/include/c++/v1/__expected/expected.h:703:14: note: in instantiation of function template specialization 'std::expected<NonCopy, int>::expecte
d<(lambda at /freebsd/data/linux/git-net/llvm-project/libcxx/test/std/utilities/expected/expected.expected/monadic/transform.pass.cpp:220:16) &, int &>' requested here
# | 703 | return expected<_Up, _Err>(__expected_construct_in_place_from_invoke_tag{}, std::forward<_Func>(__f), __repr_.__union_.__val_);
libcxx/include/__expected/expected.h
Outdated
@@ -909,11 +850,19 @@ class expected { | |||
std::__expected_construct_unexpected_from_invoke_tag, _Func&& __f, _Args&&... __args) | |||
: __unex_(std::invoke(std::forward<_Func>(__f), std::forward<_Args>(__args)...)) {} | |||
|
|||
template <class _Union> | |||
_LIBCPP_HIDE_FROM_ABI constexpr explicit __union_t(bool __has_val, _Union&& __other) { |
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.
I think we should move the construct_at
s outside the union and instead construct_at
the __expected_repr
.
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.
Hm, then the union would need to be default constructible and have an __empty_
member again. Also, ordering issues among the union and __has_val_
flag initialization would become possible once more. What do you think?
libcxx/include/__expected/expected.h
Outdated
@@ -1552,8 +1570,56 @@ class expected<_Tp, _Err> { | |||
_LIBCPP_NO_UNIQUE_ADDRESS _ErrorType __unex_; | |||
}; | |||
|
|||
_LIBCPP_NO_UNIQUE_ADDRESS __union_t<_Err> __union_; | |||
bool __has_val_; | |||
struct __expected_repr { |
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.
Let's just name this __repr
. It's already inside expected
.
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.
done!
@@ -51,6 +51,7 @@ struct __libcpp_datasizeof { | |||
// the use as an extension. | |||
_LIBCPP_DIAGNOSTIC_PUSH | |||
_LIBCPP_CLANG_DIAGNOSTIC_IGNORED("-Winvalid-offsetof") | |||
_LIBCPP_GCC_DIAGNOSTIC_IGNORED("-Winvalid-offsetof") |
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.
Where does this change come from?
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.
I noticed from earlier when playing around with __libcpp_datasizeof
that GCC errors out on the offsetof
(the CI build failed). It seems that with those diagnostic macros you need to suppress for both Clang and GCC separately because their flags may be different.
Anyway, this should probably a separate issue/PR, so I'll remove it for now.
|
What do you think about this: #include <expected>
#include <iostream>
#include <memory>
#include <optional>
template <typename Val, typename Err>
union expected_union {
[[no_unique_address]] Val val;
[[no_unique_address]] Err unex;
};
template <typename Val, typename Err, bool StuffTail = false>
struct expected_repr {
private:
expected_union<Val, Err> union_;
[[no_unique_address]] bool has_val_;
};
template <typename Val, typename Err>
struct expected_repr<Val, Err, true> {
private:
[[no_unique_address]] expected_union<Val, Err> union_;
[[no_unique_address]] bool has_val_;
};
template <typename Val, typename Err, //
typename Repr = expected_repr<Val, Err, true>,
typename Union = expected_union<Val, Err>>
concept tail_stuffable = sizeof(Repr) == sizeof(Union);
template <typename Val, typename Err>
struct expected_base {
protected:
[[no_unique_address]] expected_repr<Val, Err, false> repr_;
};
template <typename Val, typename Err>
requires tail_stuffable<Val, Err>
struct expected_base<Val, Err> {
protected:
expected_repr<Val, Err, true> repr_;
};
template <typename Val, typename Err>
struct expected : private expected_base<Val, Err> {}; https://godbolt.org/z/5oj6TjTGd You would have two What I like about this is that you don't even need something like In the "tail padding" case you would I'll try to implement this to see how bad it is regarding code complexity. But I'm optimistic! |
b5073bb
to
7b67afc
Compare
I've implemented the layout from comment #69673 (comment) and I think it works pretty well! There are now two layouts:
Some note:
|
One think I'm still unsure about is the layout of the union itself. Currently, it looks like this: template <class Val, class Err>
union {
Val val_;
Err unex_;
};
template <class Val, class Err>
requires(is_trivially_move_constructible_v<Val> && is_trivially_move_constructible_v<Err>)
union {
[[no_unique_address]] Val val_;
[[no_unique_address]] Err unex_;
}; ...i.e. tail padding is only ever reused if both types are trivially move constructible. I think this was done to make the layout compatible with GCC (and possibly future changes to the standard/Itanium ABI). itanium-cxx-abi/cxx-abi#107 has some good discussion. But I wonder, why do the types need to be trivially move constructible? Wouldn't nothrow move constructible be enough? The problematic case seems to be this one: https://godbolt.org/z/1xEo1njGP There, type To fix this, wouldn't this work instead of the current approach? template <class Val, class Err>
union u {
Val val_;
Err unex_;
};
template <class Val, class Err>
requires is_nothrow_move_constructible_v<Val>
union<Val, Err> u {
[[no_unique_address]] Val val_;
Err unex_;
};
template <class Val, class Err>
requires is_nothrow_move_constructible_v<Err>
union<Val, Err> u {
Val val_;
[[no_unique_address]] Err unex_;
};
template <class Val, class Err>
requires(is_nothrow_move_constructible_v<Val> && is_nothrow_move_constructible_v<Err>)
union<Val, Err> u {
[[no_unique_address]] Val val_;
[[no_unique_address]] Err unex_;
}; There is a bit of combinatorial explosion going on, but this the best layout I could come up with that is still compatible with GCC. |
I have convinced myself that making the union members unconditionally |
I managed to fix the code duplication by making template <bool NoUnique, class Tp>
class conditional_no_unique_address {
struct unique {
Tp v;
};
struct no_unique {
[[no_unique_address]] Tp v;
};
public:
using type = std::conditional<NoUnique, no_unique, unique>::type;
}
// Returns true iff "has value" can be stuffed into the tail of the union.
template <class Union>
constexpr bool can_stuff_tail();
template <class Tp, class Err>
class expected_base {
union union_t {
[[no_unique_address]] Tp val;
[[no_unique_address]] Err unex;
};
struct repr {
private:
// If "has value" can be stuffed into the tail, this should be
// `[[no_unique_address]]`, otherwise not.
[[no_unique_address]] conditional_no_unique_address<
can_stuff_tail<union_t>(), union_t>::type union_;
[[no_unique_address]] bool has_val_;
};
protected:
// If "has value" can be stuffed into the tail, this must _not_ be
// `[[no_unique_address]]` so that we fill out the complete `expected` object.
[[no_unique_address]] conditional_no_unique_address<
!can_stuff_tail<union_t>(), repr>::type repr_;
};
template <class Tp, class Err>
class expected : private expected_base<Tp, Err> {}; |
…form_error.pass.cpp Co-authored-by: Louis Dionne <ldionne.2@gmail.com>
…rything works" This reverts commit 4bb1d3e.
…address.compile.pass.cpp Co-authored-by: Louis Dionne <ldionne.2@gmail.com>
…_put_flag_in_tail for better readability
cc96bac
to
9664d71
Compare
Ah, I forgot to put an "XFAIL: msvc" on the "expected.void" test of "transform_error.mandates.verify.cpp". It should work now, hopefully. |
The CI failure is due to FreeBSD and is unrelated to this change. Merging. |
Currently
std::expected
can have some padding bytes in its tail due to[[no_unique_address]]
. Those padding bytes can be used by other objects. For example, in the current implementation:...so the data layout of an
std::expected<std::expected<std::optional<int>, bool>, bool>
can look like this:This is problematic because
emplace()
ing the "inner" expected can not only overwrite the "inner" expected "has value" flag (issue #68552) but also the tail padding where other objects might live.@philnik777 proposed to add a
char __padding_[]
array to the end of theexpected
to make sure that theexpected
itself never has any tail padding bytes that might get used by other objects.This is an ABI breaking change because
sizeof(std::expected<std::optional<int>, bool>) < sizeof(std::expected<std::expected<std::optional<int>, bool>, bool>);
afterwards. The data layout will change in the following cases where tail padding can be reused by other objects:or using
[[no_unique_address]]
:Fixes: #70494